Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add possibility to check only public coverage #10

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

stieglma
Copy link

No description provided.

Copy link
Owner

@manoelcampos manoelcampos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to perform some changes to the code and request other ones.
To make it easier, could you please give me temporary writing permission to your fork?

You can access this link and add me as a collaborator.

@stieglma
Copy link
Author

what do you want to change? The second pull request I issued relies already on some parts of these changes here. (This is also the version I currently use, I checked the numbers and they all seem to be correct there)

@stieglma
Copy link
Author

if it's not too much I'd prefer when you put comments to the locations you want to have changed (and also the way you want to have them change) I'll do it until sometime saturday then

@manoelcampos
Copy link
Owner

manoelcampos commented Jan 26, 2018

I haven't reviewed all the changes yet. Up to now, I performed some small changes I describe below, which are related to format and documentation only:

  1. Small documentation fixes and reorganization to conform to the general format used in the project.
    For instance, the documentation for a param should be placed after its name, not in the next line.
  2. Optimized imports: when there are more than 3 imports from the same package, an import package.* is used to avoid a long list of imports that just makes the code less clean. This approach has no impact on compiled class size, since Java compiler includes just the used classes into the bytecode. This way, the use of * is being preferred and it's automatically performed by IntelliJ.
  3. Reformated some long lambda expressions to break line after each method call. This is a good practice to make lambdas clearer.

There is a major issue which will require several structural changes. I wonder if you could fix it as I describe below (after merging the changes I've already done).

  1. You included the computeOnlyForPublicModifier attribute into the CoverageDoclet class. This attribute is being passed around several classes. Even where there is access to a CoverageDoclet object, such as inside the AbstractDataExporter class, the attribute is passed as a parameter to other classes. That is the case at line 61 of the AbstractDataExporter. The JavaDocsStats requires a RootDoc object which is got from the doclet. In the same way, the value for the computeOnlyForPublic parameter is being got from doclet. That suggests you could just pass the doclet as a single parameter and get what you want from it inside the JavaDocsStats. The same applies for all DocStats classes.
  2. Multiple methods from classes such as ClassDocStats received a new computeOnlyForPublic. When you pass the same parameter around the methods of a class, that suggests you should create an attribute and remove such parameter from the methods. For such classes, applying the approach proposed above fix this issue.

There may be other small issues. Tell me the way you prefer to approach our changes.

@stieglma
Copy link
Author

I added some rework of the docmentation and formatting stuff. As I work with eclipse it may not comply 100% to the settings you have in IntelliJ. As this is a common problem I propose to use the google-java-formatter instead. It has quite reasonable formatting defaults and will produce the same formatting for all IDEs.

To the second point regarding passing the CoverageDoclet as a parameter around: This is something I wouldn't do as it has many more methods that should not ever be called inside all the javadoc computation classes.
I was writing it this way, in order not to change the whole code at this point. But the way to go would be to create a separate configuration container class (lets call it Configuration ;) ) which contains the rootDoc and the options which are set or not. This way it would also become much easier to add new options (similar to passing the doclet, but this has just some other purpose and shouldn't be used for that case)

@manoelcampos
Copy link
Owner

Sorry for the late reply. I think that creating a Configuration class is an excellent option. I appreciate if you could follow this path. However, since the Doclet is already being passed around, a new Configuration configuration attribute could be defined inside the Doclet class. This way, the configuration could be got directly from the Doclet, not requiring to add an extra parameter to existing methods.

I'm going to include some comments on the code you pushed.

/**
* Checks if a given parameter is a valid custom parameter accepted by this doclet.
* @param paramName the name of the parameter to check
* @return true if it's a valid custom parameter, false otherwise
*/
private static boolean isCustomParameter(final String paramName) {
return isParameter(paramName, OUTPUT_NAME_OPTION);
return isParameter(paramName, OUTPUT_NAME_OPTION) || isParameter(paramName, COVERAGE_ONLY_FOR_PUBLIC_OPTION);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it should be created a new class to define a command line parameter. The class would include the short and long param names and a description. Then, a list of custom parameters could be defined, so that it can be iterated here to avoid changing the method every time a new parameter is included.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that was part of the configuration rework

}
}

return new String[]{};
return null;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid NulPointerException, I prefer returning an empty String instead of null. We can use String.isEmpty to check if there is a value for a given parameter.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't a problem with the new configuration handling any more

.count();
}

private Predicate<? super Doc> filterPublicIfNecessary() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to make the method return a Predicate. That just makes the method body more complex by requiring it to return a lambda expression. You can just change the method signature to the same signature of the test method into the Predicate functional interface: private boolean filterPublicIfNecessary(Doc m).

Then, where you have filterPublicIfNecessary() you just change to a method reference this::filterPublicIfNecessary

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

@stieglma
Copy link
Author

Configuration handling rework is now done. Took a while, but I didn't have much time the last weeks

@stieglma
Copy link
Author

I also added lombok as a dependency. I greatly reduces boilerplate code that would have to be written otherwise. If you don't like to have this dependency we should at least think about using guava for less verbose precondition checks.

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't like lombok because it uses some JDK internals which may be removed. It also requires IDE plugins for some features. At this time, I prefer to keep dependencies as minimal as possible.

@@ -75,51 +62,12 @@ public static boolean start(final RootDoc rootDoc) {
*/
public CoverageDoclet(final RootDoc rootDoc) {
this.rootDoc = rootDoc;
this.exporter = new HtmlExporter(this);
}
Configuration config = new Configuration(rootDoc.options());
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea in my previous message was to create config as an attribute. This way, you don't need to pass an additional parameter for some methods which already receive the doclet.
I think the code will be clearer if you pass the RootDoc instead of an matrix of String for the Configuration constructor. Inside the constructor you can get the options from the rootDoc.
It's not obvious where this [][] String has to be got from. On the other hand, it's straighforward to understand where to get a rootDoc object from. This way, we move the complexity into the Configuration class.

private static boolean isCustomParameter(final String paramName) {
return isParameter(paramName, OUTPUT_NAME_OPTION);
}
JavaDocsStats stats = new JavaDocsStats(rootDoc, config);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the Configuration constructor as suggested above makes unnecessary to add one more parameter to the JavaDocsStats constructor. You can pass only the config parameter and get the rootDoc from it.
For a cleaner and more functional code, the fewer parameters the better.

The stats object was being created inside the AbstractDataExporter class and you moved it out here. The object is then passed to the HtmlExporter constructor below, also increasing its number of parameters. This moves the complexity out of the AbstractDataExporter classes. Encapsulating the stats inside the AbstractDataExporter avoids duplicating its instantiation for every exporter class and also hides the creation of an object used just internally by such exporter classes.


return Standard.validOptions(options, errorReporter);
// this needs to be the last part as it already accesses some stuff from the doclet
this.exporter = new HtmlExporter(config, stats);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out the comment above.

}

/**
* Gets the values associated to a given command line option.
* Checks that all given option are valid
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Checks that all given options are valid"

private final Optional<String> defaultValue;

public StringOption(@NonNull String shortName, @NonNull String longName, @NonNull String description,
@NonNull Optional<String> defaultValue) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's expected to make methods to return an Optional, but it's a bad practice to declare parameters and fields as Optional. Since the parameter list is too long, I suggest you remove the defaultValue param and use a simple Builder pattern by adding a setter to the defaultValue as below:

StringOption setDefaultValue(String defaultValue){
   this. defaultValue = defaultValue;
   return this;
}

This way, if you need to set the default value, you can make a chained call like
StringOption option = new StringOption("-op", "-onlyPublic", "Only public members").setDefaultValue("true");

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's even worse to have mutable options, either we need to create complete builder pattern for it, or leave it in the constructor. For working around the optional in the constructor we could create two constructors, one without default value parameter, and one with, (where the default value has to be nonnull)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid to much work for you, an overloaded constructor is good and simpler.

return opt.get().getLength();
}
return Standard.optionLength(option);
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using isPresent is a bad practice also. You could replace the lines starting at the if by:

return opt.map(Option::getLength).orElseGet(() -> Standard.optionLength(option))

@@ -0,0 +1,34 @@
package com.manoelcampos.javadoc.coverage.configuration;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this StringOption class is needed. Since the Option class is generic, you could make it concrete and instantiate it. The number of values an option requires can be defined by the length attribute. This way, there is no need to add an abstract method parseValue. The Option class can implement this method by checking if the number of given parameters is equal to length.

Creating a sub-class for each type of option eliminates all the advantages of using a single generic class.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually it isn't necessary currently, but it will be necessary as soon as we have e.g. NumberOptions that need to parse the value in some or another way.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parsing process is currently just converting the String value to a specific type. Since there isn't an easy way to convert a String to a generic type (without recurring to reflection), I think the best approach would be to return the value as String and leave the conversion process for the caller. This is the approach used by Apache Commons CLI. The number of required values can be checked automatically by using the length attribute. Despite you consider that the option can have more than one value, your implementations (such as the StringOption) always return just one value.

The implementation below doesn't care about value conversion and automatically check if the required number of values was passed. It also has a method to get just the first value as well as an array of values (removing the element 0 which represents the option name). It handles default value too.

public class Option{
    private int length;
    private String defaultValue;

    public String getFirstValue(final String[] values){
        return getValues(values)[0];
    }  

    /**
     * Gets the values of the option, excluding the option name (the 0th element).
     * @return an array containing the values (without the 0th element), if the values' length isn't as expected; 
     *         or an array containing the default value if the length isn't as expected and there is a default value.
     * @throws IllegalStateException when the length of values isn't as expected and there isn't a default value
     */
    public String[] getValues(final String[] values){
        if(defaultValue == null && values.length != length){
            throw new IllegalStateException("Wrong option line passed, string options do only have 2 values");
        }

        return values.length == length ? 
                Arrays.copyOfRange(values, 1, values.length) :
                new String[]{defaultValue};
    }

    /* Trying the implementation. */
    public static void main(String[] args) {
        System.out.println();
        Option o = new Option();
        o.length = 3;
        String[] values = new String[]{"-o", "1", "2"};
        System.out.println("First Value: " + o.getFirstValue(values));
        System.out.println("Values: " + Arrays.toString(o.getValues(values)));

        try{
            values = new String[]{"-o", "1"};
            //Error because the length of values isn't equal to 3
            System.out.println("Value: " + o.getFirstValue(values));
        } catch(Exception e){
            System.out.println("\nError: " + e.getMessage() + "\n");
        }

        o.length = 2;
        o.defaultValue = "default value";
        values = new String[]{"-o"};
        System.out.println("First Value: " + o.getFirstValue(values));
        System.out.println("Values: " + Arrays.toString(o.getValues(values)));        
    }
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just updated the implementation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the value parsing is one of the most important features. Why would we allow the caller to parse the values as he likes if it can be done already in the Option itself? I added an option for minimal coverage targets as an example. Sure we could just return all the string values to the caller of getValues but then the caller has to ensure every condition again and again everytime he needs it. By doing all this stuff in the option we don't have these problem and also provide a cleaner interface.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parsing is being performed by the implementation I provided, based on your code, making sure to:

  1. check if the number of values is as required;
  2. use the default value if a value is not present;
  3. remove the parameter name from the list of values.

This is performed both for the getValue and the getValues methods. If you provide just the getValue, there is no way to get the additional values when the required number of values is greater than 1. Therefore, it doesn't make sense to consider that an option can have multiple values if the class allows access only to the first value.

The caller won't parse the values. This is done by the provided methods, which ensure the conditions enumerated above are met. They just aren't converting the value to a specific type. Since the conversion from String to a specific type is a one-liner for the caller (such as Integer.valueOf(String value)), I don't see any problem with it.

Creating a subclass simply to convert values from String is overkill. If you really want to convert the values inside the class, I see two alternatives: using reflection or functional programming.

A generic Option class using reflection is available here. The code is ugly, has to deal with lots of exceptions and there is the reflection overhead yet. However, it works for any class representing primitive values and will fail for any other type of object.

A generic, functional Option class is available here. It enables the caller to provide a Function that converts the values from String to the generic type and can work for any type you want. The only downside is that the caller is required to provide a conversion Function.

And if you want to provide default implementations for converting String to classes representing primitive types, there is this example here. It has an ugly if chain, which doesn't follow the Open/Closed Principle, but works for any primitive type.

@@ -46,41 +45,46 @@
private List<MethodDocStats> methodsStats;
private List<MethodDocStats> constructorsStats;

public ClassDocStats(final ClassDoc doc) {
public ClassDocStats(final ClassDoc doc, Configuration config) {
this.doc = doc;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The config parameter should be stored in a config attribute. This way, you avoid passing the config around several methods of the class.

}

private boolean filterPublicIfNecessary(Doc m) {
boolean computeOnlyForPublic = config.computePublicCoverageOnly();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

declare as final

@ToString
@Getter
@AllArgsConstructor
abstract class Option<T> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that it would be better to use Apache Commons Cli to parse command line arguments. But nevermind if it's to much work for you.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into it briefly before writing my own option class. What we need are generic default values, and I didn't see this capability when looking at the usage examples there

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants