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

[apex] Add validation of ApexDoc comments #1314

Merged
merged 7 commits into from
Sep 23, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/pages/pmd/rules/apex.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ folder: pmd/rules

It contains the following rules:

[ApexBadCrypto](pmd_rules_apex_security.html#apexbadcrypto), [ApexCRUDViolation](pmd_rules_apex_security.html#apexcrudviolation), [ApexCSRF](pmd_rules_apex_security.html#apexcsrf), [ApexDangerousMethods](pmd_rules_apex_security.html#apexdangerousmethods), [ApexInsecureEndpoint](pmd_rules_apex_security.html#apexinsecureendpoint), [ApexOpenRedirect](pmd_rules_apex_security.html#apexopenredirect), [ApexSharingViolations](pmd_rules_apex_security.html#apexsharingviolations), [ApexSOQLInjection](pmd_rules_apex_security.html#apexsoqlinjection), [ApexSuggestUsingNamedCred](pmd_rules_apex_security.html#apexsuggestusingnamedcred), [ApexUnitTestClassShouldHaveAsserts](pmd_rules_apex_bestpractices.html#apexunittestclassshouldhaveasserts), [ApexUnitTestShouldNotUseSeeAllDataTrue](pmd_rules_apex_bestpractices.html#apexunittestshouldnotuseseealldatatrue), [ApexXSSFromEscapeFalse](pmd_rules_apex_security.html#apexxssfromescapefalse), [ApexXSSFromURLParam](pmd_rules_apex_security.html#apexxssfromurlparam), [AvoidDeeplyNestedIfStmts](pmd_rules_apex_design.html#avoiddeeplynestedifstmts), [AvoidDirectAccessTriggerMap](pmd_rules_apex_errorprone.html#avoiddirectaccesstriggermap), [AvoidDmlStatementsInLoops](pmd_rules_apex_performance.html#avoiddmlstatementsinloops), [AvoidGlobalModifier](pmd_rules_apex_bestpractices.html#avoidglobalmodifier), [AvoidHardcodingId](pmd_rules_apex_errorprone.html#avoidhardcodingid), [AvoidLogicInTrigger](pmd_rules_apex_bestpractices.html#avoidlogicintrigger), [AvoidNonExistentAnnotations](pmd_rules_apex_errorprone.html#avoidnonexistentannotations), [AvoidSoqlInLoops](pmd_rules_apex_performance.html#avoidsoqlinloops), [AvoidSoslInLoops](pmd_rules_apex_performance.html#avoidsoslinloops), [ClassNamingConventions](pmd_rules_apex_codestyle.html#classnamingconventions), [CyclomaticComplexity](pmd_rules_apex_design.html#cyclomaticcomplexity), [EmptyCatchBlock](pmd_rules_apex_errorprone.html#emptycatchblock), [EmptyIfStmt](pmd_rules_apex_errorprone.html#emptyifstmt), [EmptyStatementBlock](pmd_rules_apex_errorprone.html#emptystatementblock), [EmptyTryOrFinallyBlock](pmd_rules_apex_errorprone.html#emptytryorfinallyblock), [EmptyWhileStmt](pmd_rules_apex_errorprone.html#emptywhilestmt), [ExcessiveClassLength](pmd_rules_apex_design.html#excessiveclasslength), [ExcessiveParameterList](pmd_rules_apex_design.html#excessiveparameterlist), [ExcessivePublicCount](pmd_rules_apex_design.html#excessivepubliccount), [ForLoopsMustUseBraces](pmd_rules_apex_codestyle.html#forloopsmustusebraces), [IfElseStmtsMustUseBraces](pmd_rules_apex_codestyle.html#ifelsestmtsmustusebraces), [IfStmtsMustUseBraces](pmd_rules_apex_codestyle.html#ifstmtsmustusebraces), [MethodNamingConventions](pmd_rules_apex_codestyle.html#methodnamingconventions), [MethodWithSameNameAsEnclosingClass](pmd_rules_apex_errorprone.html#methodwithsamenameasenclosingclass), [NcssConstructorCount](pmd_rules_apex_design.html#ncssconstructorcount), [NcssMethodCount](pmd_rules_apex_design.html#ncssmethodcount), [NcssTypeCount](pmd_rules_apex_design.html#ncsstypecount), [StdCyclomaticComplexity](pmd_rules_apex_design.html#stdcyclomaticcomplexity), [TooManyFields](pmd_rules_apex_design.html#toomanyfields), [VariableNamingConventions](pmd_rules_apex_codestyle.html#variablenamingconventions), [WhileLoopsMustUseBraces](pmd_rules_apex_codestyle.html#whileloopsmustusebraces)
[ApexBadCrypto](pmd_rules_apex_security.html#apexbadcrypto), [ApexCRUDViolation](pmd_rules_apex_security.html#apexcrudviolation), [ApexCSRF](pmd_rules_apex_security.html#apexcsrf), [ApexDangerousMethods](pmd_rules_apex_security.html#apexdangerousmethods), [ApexDoc](pmd_rules_apex_documentation.html#apexdoc), [ApexInsecureEndpoint](pmd_rules_apex_security.html#apexinsecureendpoint), [ApexOpenRedirect](pmd_rules_apex_security.html#apexopenredirect), [ApexSharingViolations](pmd_rules_apex_security.html#apexsharingviolations), [ApexSOQLInjection](pmd_rules_apex_security.html#apexsoqlinjection), [ApexSuggestUsingNamedCred](pmd_rules_apex_security.html#apexsuggestusingnamedcred), [ApexUnitTestClassShouldHaveAsserts](pmd_rules_apex_bestpractices.html#apexunittestclassshouldhaveasserts), [ApexUnitTestShouldNotUseSeeAllDataTrue](pmd_rules_apex_bestpractices.html#apexunittestshouldnotuseseealldatatrue), [ApexXSSFromEscapeFalse](pmd_rules_apex_security.html#apexxssfromescapefalse), [ApexXSSFromURLParam](pmd_rules_apex_security.html#apexxssfromurlparam), [AvoidDeeplyNestedIfStmts](pmd_rules_apex_design.html#avoiddeeplynestedifstmts), [AvoidDirectAccessTriggerMap](pmd_rules_apex_errorprone.html#avoiddirectaccesstriggermap), [AvoidDmlStatementsInLoops](pmd_rules_apex_performance.html#avoiddmlstatementsinloops), [AvoidGlobalModifier](pmd_rules_apex_bestpractices.html#avoidglobalmodifier), [AvoidHardcodingId](pmd_rules_apex_errorprone.html#avoidhardcodingid), [AvoidLogicInTrigger](pmd_rules_apex_bestpractices.html#avoidlogicintrigger), [AvoidNonExistentAnnotations](pmd_rules_apex_errorprone.html#avoidnonexistentannotations), [AvoidSoqlInLoops](pmd_rules_apex_performance.html#avoidsoqlinloops), [AvoidSoslInLoops](pmd_rules_apex_performance.html#avoidsoslinloops), [ClassNamingConventions](pmd_rules_apex_codestyle.html#classnamingconventions), [CyclomaticComplexity](pmd_rules_apex_design.html#cyclomaticcomplexity), [EmptyCatchBlock](pmd_rules_apex_errorprone.html#emptycatchblock), [EmptyIfStmt](pmd_rules_apex_errorprone.html#emptyifstmt), [EmptyStatementBlock](pmd_rules_apex_errorprone.html#emptystatementblock), [EmptyTryOrFinallyBlock](pmd_rules_apex_errorprone.html#emptytryorfinallyblock), [EmptyWhileStmt](pmd_rules_apex_errorprone.html#emptywhilestmt), [ExcessiveClassLength](pmd_rules_apex_design.html#excessiveclasslength), [ExcessiveParameterList](pmd_rules_apex_design.html#excessiveparameterlist), [ExcessivePublicCount](pmd_rules_apex_design.html#excessivepubliccount), [ForLoopsMustUseBraces](pmd_rules_apex_codestyle.html#forloopsmustusebraces), [IfElseStmtsMustUseBraces](pmd_rules_apex_codestyle.html#ifelsestmtsmustusebraces), [IfStmtsMustUseBraces](pmd_rules_apex_codestyle.html#ifstmtsmustusebraces), [MethodNamingConventions](pmd_rules_apex_codestyle.html#methodnamingconventions), [MethodWithSameNameAsEnclosingClass](pmd_rules_apex_errorprone.html#methodwithsamenameasenclosingclass), [NcssConstructorCount](pmd_rules_apex_design.html#ncssconstructorcount), [NcssMethodCount](pmd_rules_apex_design.html#ncssmethodcount), [NcssTypeCount](pmd_rules_apex_design.html#ncsstypecount), [StdCyclomaticComplexity](pmd_rules_apex_design.html#stdcyclomaticcomplexity), [TooManyFields](pmd_rules_apex_design.html#toomanyfields), [VariableNamingConventions](pmd_rules_apex_codestyle.html#variablenamingconventions), [WhileLoopsMustUseBraces](pmd_rules_apex_codestyle.html#whileloopsmustusebraces)

* Empty Code (`rulesets/apex/empty.xml`):

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import apex.jorje.semantic.ast.AstNode;

public abstract class ApexRootNode<T extends AstNode> extends AbstractApexNode<T> implements RootNode {
private String source;

public ApexRootNode(T node) {
super(node);
}
Expand All @@ -21,4 +23,13 @@ void calculateLineNumbers(SourceCodePositioner positioner) {
this.endLine = positioner.getLastLine();
this.endColumn = positioner.getLastLineColumn();
}

@Override
protected void handleSourceCode(String source) {
this.source = source;
}

public String getSource() {
return source;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/

package net.sourceforge.pmd.lang.apex.rule.documentation;

import static apex.jorje.semantic.symbol.type.ModifierTypeInfos.GLOBAL;
import static apex.jorje.semantic.symbol.type.ModifierTypeInfos.OVERRIDE;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.antlr.runtime.ANTLRStringStream;
import org.antlr.runtime.Token;

import net.sourceforge.pmd.lang.apex.ast.ASTAnnotation;
import net.sourceforge.pmd.lang.apex.ast.ASTMethod;
import net.sourceforge.pmd.lang.apex.ast.ASTModifierNode;
import net.sourceforge.pmd.lang.apex.ast.ASTProperty;
import net.sourceforge.pmd.lang.apex.ast.ASTUserClass;
import net.sourceforge.pmd.lang.apex.ast.ASTUserInterface;
import net.sourceforge.pmd.lang.apex.ast.ApexNode;
import net.sourceforge.pmd.lang.apex.ast.ApexRootNode;
import net.sourceforge.pmd.lang.apex.rule.AbstractApexRule;

import apex.jorje.data.Locations;
import apex.jorje.parser.impl.ApexLexer;
import apex.jorje.semantic.ast.member.Parameter;
import apex.jorje.semantic.ast.modifier.ModifierGroup;

public class ApexDocRule extends AbstractApexRule {
private static final Pattern DESCRIPTION_PATTERN = Pattern.compile("@description\\s");
private static final Pattern RETURN_PATTERN = Pattern.compile("@return\\s");
private static final Pattern PARAM_PATTERN = Pattern.compile("@param\\s+(\\w+)\\s");

private static final String MISSING_COMMENT_MESSAGE = "Missing ApexDoc comment";
private static final String MISSING_DESCRIPTION_MESSAGE = "Missing ApexDoc @description";
private static final String MISSING_RETURN_MESSAGE = "Missing ApexDoc @return";
private static final String UNEXPECTED_RETURN_MESSAGE = "Unexpected ApexDoc @return";
private static final String MISMATCHED_PARAM_MESSAGE = "Missing or mismatched ApexDoc @param";

private boolean inClass;
private boolean inTestClass;
private String source;
private List<TokenLocation> tokenLocations;

@Override
public Object visit(ASTUserClass node, Object data) {
if (inClass) {
super.visit(node, data);
} else {
inClass = true;
inTestClass = false;
buildTokens(node);
super.visit(node, data);
inClass = false;
}

handleClassOrInterface(node, data);

return data;
}


@Override
public Object visit(ASTUserInterface node, Object data) {
if (inClass) {
super.visit(node, data);
} else {
buildTokens(node);
super.visit(node, data);
}

handleClassOrInterface(node, data);

return data;
}

@Override
public Object visit(ASTAnnotation node, Object data) {
if (node.getImage().equals("IsTest")) {
inTestClass = true;
Copy link
Member

Choose a reason for hiding this comment

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

also, this variable is never being reset

}
return data;
}

@Override
public Object visit(ASTMethod node, Object data) {
ApexDocComment comment = getApexDocComment(node);
if (comment == null) {
if (shouldHaveApexDocs(node)) {
addViolationWithMessage(data, node, MISSING_COMMENT_MESSAGE);
}
} else {
if (!comment.hasDescription) {
addViolationWithMessage(data, node, MISSING_DESCRIPTION_MESSAGE);
}

String returnType = node.getNode().getReturnTypeRef().toString();
boolean shouldHaveReturn = !(returnType.isEmpty() || "void".equalsIgnoreCase(returnType));
if (comment.hasReturn != shouldHaveReturn) {
if (shouldHaveReturn) {
addViolationWithMessage(data, node, MISSING_RETURN_MESSAGE);
} else {
addViolationWithMessage(data, node, UNEXPECTED_RETURN_MESSAGE);
}
}

ArrayList<String> params = new ArrayList<>();
for (Parameter x : node.getNode().getMethodInfo().getParameters()) {
String value = x.getName().getValue();
params.add(value);
}

if (!comment.params.equals(params)) {
addViolationWithMessage(data, node, MISMATCHED_PARAM_MESSAGE);
}
}

return data;
}

@Override
public Object visit(ASTProperty node, Object data) {
ApexDocComment comment = getApexDocComment(node);
if (comment == null) {
if (shouldHaveApexDocs(node)) {
addViolationWithMessage(data, node, MISSING_COMMENT_MESSAGE);
}
} else {
if (!comment.hasDescription) {
addViolationWithMessage(data, node, MISSING_DESCRIPTION_MESSAGE);
}
}

return data;
}

private void buildTokens(ApexRootNode<?> node) {
source = node.getSource();
ANTLRStringStream stream = new ANTLRStringStream(source);
ApexLexer lexer = new ApexLexer(stream);

tokenLocations = new ArrayList<>();
Integer startIndex = 0;
Token token = lexer.nextToken();
Integer endIndex = lexer.getCharIndex();
while (token.getType() != Token.EOF) {
if (token.getType() != ApexLexer.WS) {
tokenLocations.add(new TokenLocation(startIndex, token.getText()));
}
startIndex = endIndex;
token = lexer.nextToken();
endIndex = lexer.getCharIndex();
}
}

private void handleClassOrInterface(ApexNode<?> node, Object data) {
ApexDocComment comment = getApexDocComment(node);
if (comment == null) {
if (shouldHaveApexDocs(node)) {
addViolationWithMessage(data, node, MISSING_COMMENT_MESSAGE);
}
} else {
if (!comment.hasDescription) {
addViolationWithMessage(data, node, MISSING_DESCRIPTION_MESSAGE);
}
}
}

private boolean shouldHaveApexDocs(ApexNode<?> node) {
if (inTestClass || node.getNode().getLoc() == Locations.NONE) {
return false;
}
ASTModifierNode modifier = node.getFirstChildOfType(ASTModifierNode.class);
if (modifier != null) {
boolean isPublic = modifier.isPublic();
ModifierGroup modifierGroup = modifier.getNode().getModifiers();
boolean isGlobal = modifierGroup.has(GLOBAL);
boolean isOverride = modifierGroup.has(OVERRIDE);
return (isPublic || isGlobal) && !isOverride;
}
return false;
}

private ApexDocComment getApexDocComment(ApexNode<?> node) {
String token = getApexDocToken(getApexDocIndex(node));
if (token == null) {
return null;
}

boolean hasDescription = DESCRIPTION_PATTERN.matcher(token).find();
boolean hasReturn = RETURN_PATTERN.matcher(token).find();

ArrayList<String> params = new ArrayList<>();
Matcher paramMatcher = PARAM_PATTERN.matcher(token);
while (paramMatcher.find()) {
params.add(paramMatcher.group(1));
}

return new ApexDocComment(hasDescription, hasReturn, params);
}

private int getApexDocIndex(ApexNode<?> node) {
ASTAnnotation annotation = node.getFirstDescendantOfType(ASTAnnotation.class);
ApexNode<?> firstNode = annotation == null ? node : annotation;
int index = firstNode.getNode().getLoc().getStartIndex();
return source.lastIndexOf('\n', index);
}

private String getApexDocToken(int index) {
TokenLocation last = null;
for (TokenLocation location : tokenLocations) {
if (location.index >= index) {
if (last != null && last.token.startsWith("/**")) {
return last.token;
}
return null;
}
last = location;
}
return null;
}

private class TokenLocation {
int index;
String token;

TokenLocation(int index, String token) {
this.index = index;
this.token = token;
}
}

private class ApexDocComment {
Copy link
Member

Choose a reason for hiding this comment

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

This should be a private static class

boolean hasDescription;
boolean hasReturn;
List<String> params;

ApexDocComment(boolean hasDescription, boolean hasReturn, List<String> params) {
this.hasDescription = hasDescription;
this.hasReturn = hasReturn;
this.params = params;
}
}
}
19 changes: 19 additions & 0 deletions pmd-apex/src/main/resources/category/apex/documentation.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,23 @@
<description>
Rules that are related to code documentation.
</description>

<rule name="ApexDoc"
since="9.9.9"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Which version?

Copy link
Member

Choose a reason for hiding this comment

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

At the time, the next release would be 6.7.0

message="ApexDoc comment is missing or incorrect"
class="net.sourceforge.pmd.lang.apex.rule.documentation.ApexDocRule"
externalInfoUrl="${pmd.website.baseurl}/pmd_rules_apex_documentation.html#apexdoc">
<description>
Identifies missing or incorrect ApexDoc comments.
Copy link
Member

Choose a reason for hiding this comment

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

We should enhance the description from your PR description:

This rule validates that:

  • ApexDoc comments are present for classes, methods, and properties that are public or global, excluding
    overrides and test classes (as well as the contents of test classes).
  • ApexDoc comments should contain @description.
  • ApexDoc comments on non-void, non-constructor methods should contain @return.
  • ApexDoc comments on void or constructor methods should not contain @return.
  • ApexDoc comments on methods with parameters should contain @param for each parameter, in the same
    order as the method signature.

</description>
<priority>3</priority>
<example>
<![CDATA[
/**
* @description Hello World
*/
]]>
</example>
</rule>

</ruleset>
11 changes: 11 additions & 0 deletions pmd-apex/src/main/resources/rulesets/apex/ruleset.xml
Original file line number Diff line number Diff line change
Expand Up @@ -419,4 +419,15 @@
<property name="cc_block_highlighting" value="false" />
</properties>
</rule>

<!-- DOCUMENTATION -->
<rule ref="category/apex/documentation.xml/ApexDoc" message="Document classes, methods, and properties that are public or global.">
<priority>3</priority>
<properties>
<!-- relevant for Code Climate output only -->
<property name="cc_categories" value="Style" />
<property name="cc_remediation_points_multiplier" value="50" />
Copy link
Contributor Author

Choose a reason for hiding this comment

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

What should the multiplier be?

Copy link
Member

Choose a reason for hiding this comment

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

See here for explanation of remediation points: https://github.com/codeclimate/spec/blob/master/SPEC.md#remediation-points

In the implementation of our CodeClimateRenderer, we use the baseline of 50_000 (which is the estimated required time for a trivial fix) and multiply it with the property defined here: https://github.com/pmd/pmd/blob/master/pmd-core/src/main/java/net/sourceforge/pmd/renderers/CodeClimateRenderer.java#L124-L132

I see, that we use multipliers between 1 and 250. Fixing missing documentation can be time consuming and we provide here a rough guess across all types of missing doc (class documentation might take more effort than a property documentation).

IMHO, 50 sounds like a reasonable value to me.

<property name="cc_block_highlighting" value="false" />
</properties>
</rule>
</ruleset>
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ rulesets.filenames=\
category/apex/bestpractices.xml,\
category/apex/codestyle.xml,\
category/apex/design.xml,\
category/apex/documentation.xml,\
category/apex/errorprone.xml,\
category/apex/performance.xml,\
category/apex/security.xml

#
# categories with no rules yet
#
#category/apex/documentation.xml
#category/apex/multithreading.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/

package net.sourceforge.pmd.lang.apex.rule.documentation;

import net.sourceforge.pmd.testframework.SimpleAggregatorTst;

public class DocumentationRulesTest extends SimpleAggregatorTst {

private static final String RULESET = "category/apex/documentation.xml";

@Override
public void setUp() {
addRule(RULESET, "ApexDoc");
}
}
Loading