typesWithConnectApiDeserializers;
+ private boolean hasInternalSfdc;
+ private boolean isRunningTests;
+ private boolean hasPrivateApi;
+ private boolean isTrustedApplication;
+ private boolean hasLocalizedTranslation;
+ private boolean isSfdc;
+ private boolean isReallyRunningTests;
+ private boolean hasApexGenericTypes;
+ private boolean hasRemoteActionPerm;
+
+ public TestAccessEvaluator() {
+ validPageVersions = HashMultimap.create();
+ visibleSetupEntitiesToTypes = HashMultimap.create();
+ managedPackagesNotInstalled = new HashSet<>();
+ accessibleSystemNamespaces = new HashSet<>();
+ orgPerm = new HashSet<>();
+ allowedPermGuards = new HashSet<>();
+ reservedNamespaces = new HashSet<>();
+ globalComponents = new HashSet<>();
+ typesWithConnectApiDeserializers = new HashSet<>();
+ hasRemoteActionPerm = true;
+ }
+
+ @Override
+ public boolean hasPermission(final PlaceholderOrgPerm orgPerm) {
+ return this.orgPerm.contains(orgPerm);
+ }
+
+ @Override
+ public boolean hasPermissionForPermGuard(final Namespace referencingNamespace, final String orgPerm) {
+ return allowedPermGuards.contains(new AllowedPermGuard(referencingNamespace, orgPerm));
+ }
+
+ @Override
+ public boolean hasPrivateApi() {
+ return hasPrivateApi;
+ }
+
+ @Override
+ public boolean hasLocalizedTranslation() {
+ return hasLocalizedTranslation;
+ }
+
+ @Override
+ public boolean hasInternalSfdc() {
+ return hasInternalSfdc;
+ }
+
+ @Override
+ public boolean isTrustedApplication() {
+ return isTrustedApplication;
+ }
+
+ @Override
+ public boolean isReservedNamespace(final Namespace namespace) {
+ return reservedNamespaces.contains(namespace);
+ }
+
+ @Override
+ public boolean isReservedNamespace(final Namespace namespace, final boolean excludePackages) {
+ return reservedNamespaces.contains(namespace);
+ }
+
+ /**
+ * See {@link #isAccessibleOrTrustedNamespace(Namespace)}
+ */
+ @Override
+ public boolean isAccessibleSystemNamespace(final Namespace namespace) {
+ return accessibleSystemNamespaces.contains(namespace);
+ }
+
+ /**
+ * Okay so this check and its partner isAccessibleSystemNamespace are used slightly differently.
+ * This is like a black list check, that prevents referencing code from seeing things in a reserved namespace.
+ * The other check allows code to see certain things if the code's namespace is a reserved namespace.
+ *
+ * Hence here we return true by default, and the {@link #isAccessibleSystemNamespace(Namespace)} returns false
+ * by default.
+ */
+ @Override
+ public boolean isAccessibleOrTrustedNamespace(final Namespace namespace) {
+ return true;
+ }
+
+ @Override
+ public boolean isRunningTests() {
+ return isRunningTests;
+ }
+
+ @Override
+ public boolean isReallyRunningTests() {
+ return isReallyRunningTests;
+ }
+
+ @Override
+ public boolean isSfdc() {
+ return isSfdc;
+ }
+
+ @Override
+ public boolean hasApexParameterizedTypes() {
+ return hasApexGenericTypes;
+ }
+
+ @Override
+ public boolean isValidPackageVersion(final Namespace namespace, final StructuredVersion version) {
+ return validPageVersions.containsEntry(namespace, version);
+ }
+
+ /**
+ * @return 'true' for everything EXCEPT namespaces you've added through {@link #addManagedPackageNotInstalled(Namespace)}
+ */
+ @Override
+ public boolean isManagedPackageInstalled(final Namespace namespace) {
+ return !managedPackagesNotInstalled.contains(namespace);
+ }
+
+ @Override
+ public boolean isSetupEntityVisibleToType(final SObjectTypeInfo type, final TypeInfo referencingType) {
+ final TypeInfo visibleReferencingType = Iterables.getFirst(visibleSetupEntitiesToTypes.get(type), null);
+ return visibleReferencingType != null
+ && visibleReferencingType.getBytecodeName().equals(referencingType.getBytecodeName());
+ }
+
+ @Override
+ public boolean hasConnectDeserializer(final TypeInfo type) {
+ return typesWithConnectApiDeserializers.contains(type.getApexName());
+ }
+
+ @Override
+ public boolean hasRemoteAction(final TypeInfo type) {
+ return false;
+ }
+
+ @Override
+ public boolean hasRemoteActionPerm() {
+ return hasRemoteActionPerm;
+ }
+
+ @Override
+ public boolean isGlobalComponent(final TypeInfo type) {
+ return globalComponents.contains(type.getApexName());
+ }
+
+ /**
+ * Things isManagedPackageInstalled will say 'false' to.
+ */
+ public TestAccessEvaluator addManagedPackageNotInstalled(final Namespace namespace) {
+ managedPackagesNotInstalled.add(namespace);
+ return this;
+ }
+
+ public TestAccessEvaluator addReservedNamespace(final Namespace namespace) {
+ reservedNamespaces.add(namespace);
+ return this;
+ }
+
+ public TestAccessEvaluator addPermission(final PlaceholderOrgPerm orgPerm) {
+ this.orgPerm.add(orgPerm);
+ return this;
+ }
+
+ public TestAccessEvaluator setHasInternalSfdc(final boolean hasInternalSfdc) {
+ this.hasInternalSfdc = hasInternalSfdc;
+ return this;
+ }
+
+ public TestAccessEvaluator addValidPackageVersion(final Namespace namespace, final StructuredVersion version) {
+ validPageVersions.put(namespace, version);
+ return this;
+ }
+
+ public TestAccessEvaluator addSetupEntityVisibleToType(
+ final SObjectTypeInfo type,
+ final String typeName
+ ) {
+ final StandardTypeInfo typeInfo = StandardTypeInfoImpl.builder()
+ .setApexName(typeName)
+ .setBytecodeName(typeName)
+ .buildResolved();
+ visibleSetupEntitiesToTypes.put(type, typeInfo);
+ return this;
+ }
+
+ public TestAccessEvaluator setIsRunningTests(final boolean isRunningTests) {
+ this.isRunningTests = isRunningTests;
+ return this;
+ }
+
+ public TestAccessEvaluator setHasPrivateApi(final boolean hasPrivateApi) {
+ this.hasPrivateApi = hasPrivateApi;
+ return this;
+ }
+
+ public TestAccessEvaluator setIsTrustedApplication(final boolean isTrustedApplication) {
+ this.isTrustedApplication = isTrustedApplication;
+ return this;
+ }
+
+ public TestAccessEvaluator setHasLocalizedTranslation(final boolean hasLocalizedTranslation) {
+ this.hasLocalizedTranslation = hasLocalizedTranslation;
+ return this;
+ }
+
+ public TestAccessEvaluator setIsSfdc(final boolean isSfdc) {
+ this.isSfdc = isSfdc;
+ return this;
+ }
+
+ public TestAccessEvaluator setIsReallyRunningTests(final boolean isReallyRunningTests) {
+ this.isReallyRunningTests = isReallyRunningTests;
+ return this;
+ }
+
+ public TestAccessEvaluator setAccessibleSystemNamespace(final Namespace namespace) {
+ accessibleSystemNamespaces.add(namespace);
+ return this;
+ }
+
+ public TestAccessEvaluator setHasApexGenericType(final boolean hasApexGenericTypes) {
+ this.hasApexGenericTypes = hasApexGenericTypes;
+ return this;
+ }
+
+ public TestAccessEvaluator allowPermGuard(final Namespace namespace, final String permGuard) {
+ allowedPermGuards.add(new AllowedPermGuard(namespace, permGuard));
+ return this;
+ }
+
+ /**
+ * It appears that remote action is enabled by default in most orgs, at least test orgs.
+ * So we will behave the same.
+ */
+ public TestAccessEvaluator setHasRemoteActionPerm(final boolean hasRemoteActionPerm) {
+ this.hasRemoteActionPerm = hasRemoteActionPerm;
+ return this;
+ }
+
+ public TestAccessEvaluator setTypeWithConnectApiDeserializer(final String typeName) {
+ typesWithConnectApiDeserializers.add(typeName);
+ return this;
+ }
+
+ public void setGlobalComponent(final String globalComponent) {
+ globalComponents.add(globalComponent);
+ }
+
+ private static class AllowedPermGuard {
+ private final Namespace referencingNamespace;
+ private final String permGuard;
+
+ AllowedPermGuard(final Namespace namespace, final String permGuard) {
+ referencingNamespace = namespace;
+ this.permGuard = permGuard;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(referencingNamespace, permGuard);
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ final AllowedPermGuard other = (AllowedPermGuard) obj;
+ return Objects.equals(referencingNamespace, other.referencingNamespace)
+ && Objects.equals(permGuard, other.permGuard);
+ }
+ }
+}
diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/TestQueryValidators.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/TestQueryValidators.java
new file mode 100644
index 000000000..96ee968a9
--- /dev/null
+++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/TestQueryValidators.java
@@ -0,0 +1,83 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+// Note: taken from https://github.com/forcedotcom/idecore/blob/3083815933c2d015d03417986f57bd25786d58ce/com.salesforce.ide.apex.core/src/apex/jorje/semantic/common/TestQueryValidators.java
+
+/*
+ * Copyright 2016 salesforce.com, inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package net.sourceforge.pmd.lang.apex.ast;
+
+import apex.jorje.semantic.ast.expression.SoqlExpression;
+import apex.jorje.semantic.ast.expression.SoslExpression;
+import apex.jorje.semantic.ast.visitor.ValidationScope;
+import apex.jorje.semantic.compiler.sfdc.QueryValidator;
+import apex.jorje.semantic.symbol.resolver.SymbolResolver;
+
+/**
+ * The test query validators will return back the query it was given. The real implementation actually creates its own
+ * query.
+ *
+ * @author jspagnola
+ */
+public final class TestQueryValidators {
+
+ private TestQueryValidators() {
+ }
+
+ public static class Noop implements QueryValidator {
+ @Override
+ public String validateSoql(
+ final SymbolResolver symbols,
+ final ValidationScope scope,
+ final SoqlExpression soql
+ ) {
+ return soql.getCanonicalQuery();
+ }
+
+ @Override
+ public String validateSosl(
+ final SymbolResolver symbols,
+ final ValidationScope typeInfo,
+ final SoslExpression sosl
+ ) {
+ return sosl.getCanonicalQuery();
+ }
+ }
+
+ public static class Error implements QueryValidator {
+ @Override
+ public String validateSoql(
+ final SymbolResolver symbols,
+ final ValidationScope scope,
+ final SoqlExpression soql
+ ) {
+ scope.getErrors().markInvalid(soql, "Bad Soql");
+ return soql.getCanonicalQuery();
+ }
+
+ @Override
+ public String validateSosl(
+ final SymbolResolver symbols,
+ final ValidationScope scope,
+ final SoslExpression sosl
+ ) {
+ scope.getErrors().markInvalid(sosl, "Bad Sosl");
+ return sosl.getCanonicalQuery();
+ }
+ }
+}
diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/AbstractApexRule.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/AbstractApexRule.java
index b51210428..0984252fb 100644
--- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/AbstractApexRule.java
+++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/AbstractApexRule.java
@@ -12,6 +12,7 @@
import net.sourceforge.pmd.lang.apex.ApexLanguageModule;
import net.sourceforge.pmd.lang.apex.ApexParserOptions;
import net.sourceforge.pmd.lang.apex.ast.ASTAnnotation;
+import net.sourceforge.pmd.lang.apex.ast.ASTAnnotationParameter;
import net.sourceforge.pmd.lang.apex.ast.ASTAnonymousClass;
import net.sourceforge.pmd.lang.apex.ast.ASTArrayLoadExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTArrayStoreExpression;
@@ -22,8 +23,10 @@
import net.sourceforge.pmd.lang.apex.ast.ASTBooleanExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTBreakStatement;
import net.sourceforge.pmd.lang.apex.ast.ASTBridgeMethodCreator;
+import net.sourceforge.pmd.lang.apex.ast.ASTCastExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTCatchBlockStatement;
import net.sourceforge.pmd.lang.apex.ast.ASTClassRefExpression;
+import net.sourceforge.pmd.lang.apex.ast.ASTConstructorPreamble;
import net.sourceforge.pmd.lang.apex.ast.ASTConstructorPreambleStatement;
import net.sourceforge.pmd.lang.apex.ast.ASTContinueStatement;
import net.sourceforge.pmd.lang.apex.ast.ASTDmlDeleteStatement;
@@ -33,7 +36,6 @@
import net.sourceforge.pmd.lang.apex.ast.ASTDmlUpdateStatement;
import net.sourceforge.pmd.lang.apex.ast.ASTDmlUpsertStatement;
import net.sourceforge.pmd.lang.apex.ast.ASTDoLoopStatement;
-import net.sourceforge.pmd.lang.apex.ast.ASTDottedExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTExpressionStatement;
import net.sourceforge.pmd.lang.apex.ast.ASTField;
@@ -43,20 +45,26 @@
import net.sourceforge.pmd.lang.apex.ast.ASTForLoopStatement;
import net.sourceforge.pmd.lang.apex.ast.ASTIfBlockStatement;
import net.sourceforge.pmd.lang.apex.ast.ASTIfElseBlockStatement;
+import net.sourceforge.pmd.lang.apex.ast.ASTIllegalStoreExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTInstanceOfExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTJavaMethodCallExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTJavaVariableExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTLiteralExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTMapEntryNode;
import net.sourceforge.pmd.lang.apex.ast.ASTMethod;
+import net.sourceforge.pmd.lang.apex.ast.ASTMethodBlockStatement;
import net.sourceforge.pmd.lang.apex.ast.ASTMethodCallExpression;
+import net.sourceforge.pmd.lang.apex.ast.ASTModifier;
import net.sourceforge.pmd.lang.apex.ast.ASTModifierNode;
import net.sourceforge.pmd.lang.apex.ast.ASTModifierOrAnnotation;
+import net.sourceforge.pmd.lang.apex.ast.ASTMultiStatement;
+import net.sourceforge.pmd.lang.apex.ast.ASTNestedExpression;
+import net.sourceforge.pmd.lang.apex.ast.ASTNestedStoreExpression;
+import net.sourceforge.pmd.lang.apex.ast.ASTNewKeyValueObjectExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTNewListInitExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTNewListLiteralExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTNewMapInitExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTNewMapLiteralExpression;
-import net.sourceforge.pmd.lang.apex.ast.ASTNewNameValueObjectExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTNewObjectExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTNewSetInitExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTNewSetLiteralExpression;
@@ -72,10 +80,10 @@
import net.sourceforge.pmd.lang.apex.ast.ASTSoslExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTStandardCondition;
import net.sourceforge.pmd.lang.apex.ast.ASTStatement;
+import net.sourceforge.pmd.lang.apex.ast.ASTStatementExecuted;
import net.sourceforge.pmd.lang.apex.ast.ASTSuperMethodCallExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTSuperVariableExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTTernaryExpression;
-import net.sourceforge.pmd.lang.apex.ast.ASTTestNode;
import net.sourceforge.pmd.lang.apex.ast.ASTThisMethodCallExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTThisVariableExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTThrowStatement;
@@ -345,11 +353,6 @@ public Object visit(ASTDmlUpsertStatement node, Object data) {
return visit((ApexNode>) node, data);
}
- @Override
- public Object visit(ASTDottedExpression node, Object data) {
- return visit((ApexNode>) node, data);
- }
-
@Override
public Object visit(ASTExpression node, Object data) {
return visit((ApexNode>) node, data);
@@ -415,11 +418,6 @@ public Object visit(ASTNewMapLiteralExpression node, Object data) {
return visit((ApexNode>) node, data);
}
- @Override
- public Object visit(ASTNewNameValueObjectExpression node, Object data) {
- return visit((ApexNode>) node, data);
- }
-
@Override
public Object visit(ASTNewObjectExpression node, Object data) {
return visit((ApexNode>) node, data);
@@ -490,11 +488,6 @@ public Object visit(ASTSuperVariableExpression node, Object data) {
return visit((ApexNode>) node, data);
}
- @Override
- public Object visit(ASTTestNode node, Object data) {
- return visit((ApexNode>) node, data);
- }
-
@Override
public Object visit(ASTThisMethodCallExpression node, Object data) {
return visit((ApexNode>) node, data);
@@ -534,4 +527,59 @@ public Object visit(ASTVariableDeclarationStatements node, Object data) {
public Object visit(ASTVariableExpression node, Object data) {
return visit((ApexNode>) node, data);
}
+
+ @Override
+ public Object visit(ASTAnnotationParameter node, Object data) {
+ return visit((ApexNode>) node, data);
+ }
+
+ @Override
+ public Object visit(ASTCastExpression node, Object data) {
+ return visit((ApexNode>) node, data);
+ }
+
+ @Override
+ public Object visit(ASTConstructorPreamble node, Object data) {
+ return visit((ApexNode>) node, data);
+ }
+
+ @Override
+ public Object visit(ASTIllegalStoreExpression node, Object data) {
+ return visit((ApexNode>) node, data);
+ }
+
+ @Override
+ public Object visit(ASTMethodBlockStatement node, Object data) {
+ return visit((ApexNode>) node, data);
+ }
+
+ @Override
+ public Object visit(ASTModifier node, Object data) {
+ return visit((ApexNode>) node, data);
+ }
+
+ @Override
+ public Object visit(ASTMultiStatement node, Object data) {
+ return visit((ApexNode>) node, data);
+ }
+
+ @Override
+ public Object visit(ASTNestedExpression node, Object data) {
+ return visit((ApexNode>) node, data);
+ }
+
+ @Override
+ public Object visit(ASTNestedStoreExpression node, Object data) {
+ return visit((ApexNode>) node, data);
+ }
+
+ @Override
+ public Object visit(ASTNewKeyValueObjectExpression node, Object data) {
+ return visit((ApexNode>) node, data);
+ }
+
+ @Override
+ public Object visit(ASTStatementExecuted node, Object data) {
+ return visit((ApexNode>) node, data);
+ }
}
diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/security/ApexCRUDViolationRule.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/security/ApexCRUDViolationRule.java
index 959e11fb0..0cc1d4634 100644
--- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/security/ApexCRUDViolationRule.java
+++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/security/ApexCRUDViolationRule.java
@@ -25,14 +25,13 @@
import net.sourceforge.pmd.lang.apex.ast.ASTDmlMergeStatement;
import net.sourceforge.pmd.lang.apex.ast.ASTDmlUpdateStatement;
import net.sourceforge.pmd.lang.apex.ast.ASTDmlUpsertStatement;
-import net.sourceforge.pmd.lang.apex.ast.ASTDottedExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTField;
import net.sourceforge.pmd.lang.apex.ast.ASTFieldDeclaration;
import net.sourceforge.pmd.lang.apex.ast.ASTFieldDeclarationStatements;
import net.sourceforge.pmd.lang.apex.ast.ASTIfElseBlockStatement;
import net.sourceforge.pmd.lang.apex.ast.ASTMethod;
import net.sourceforge.pmd.lang.apex.ast.ASTMethodCallExpression;
-import net.sourceforge.pmd.lang.apex.ast.ASTNewNameValueObjectExpression;
+import net.sourceforge.pmd.lang.apex.ast.ASTNewKeyValueObjectExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTParameter;
import net.sourceforge.pmd.lang.apex.ast.ASTProperty;
import net.sourceforge.pmd.lang.apex.ast.ASTReferenceExpression;
@@ -326,16 +325,13 @@ private void collectCRUDMethodLevelChecks(final ASTMethodCallExpression node) {
}
// see if getDescribe()
- final ASTDottedExpression dottedExpr = ref.getFirstChildOfType(ASTDottedExpression.class);
- if (dottedExpr != null) {
- final ASTMethodCallExpression nestedMethodCall = dottedExpr
- .getFirstChildOfType(ASTMethodCallExpression.class);
- if (nestedMethodCall != null) {
- if (isLastMethodName(nestedMethodCall, S_OBJECT_TYPE, GET_DESCRIBE)) {
- String resolvedType = getType(nestedMethodCall);
- if (!typeToDMLOperationMapping.get(resolvedType).contains(method)) {
- typeToDMLOperationMapping.put(resolvedType, method);
- }
+ final ASTMethodCallExpression nestedMethodCall = ref
+ .getFirstChildOfType(ASTMethodCallExpression.class);
+ if (nestedMethodCall != null) {
+ if (isLastMethodName(nestedMethodCall, S_OBJECT_TYPE, GET_DESCRIBE)) {
+ String resolvedType = getType(nestedMethodCall);
+ if (!typeToDMLOperationMapping.get(resolvedType).contains(method)) {
+ typeToDMLOperationMapping.put(resolvedType, method);
}
}
}
@@ -392,7 +388,7 @@ private void checkForCRUD(final AbstractApexNode> node, final Object data, fin
return;
}
- final ASTNewNameValueObjectExpression newObj = node.getFirstChildOfType(ASTNewNameValueObjectExpression.class);
+ final ASTNewKeyValueObjectExpression newObj = node.getFirstChildOfType(ASTNewKeyValueObjectExpression.class);
if (newObj != null) {
final String type = Helper.getFQVariableName(newObj);
validateCRUDCheckPresent(node, data, crudMethod, type);
diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/security/Helper.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/security/Helper.java
index c1a0869c8..9a266138f 100644
--- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/security/Helper.java
+++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/security/Helper.java
@@ -14,12 +14,11 @@
import net.sourceforge.pmd.lang.apex.ast.ASTDmlUndeleteStatement;
import net.sourceforge.pmd.lang.apex.ast.ASTDmlUpdateStatement;
import net.sourceforge.pmd.lang.apex.ast.ASTDmlUpsertStatement;
-import net.sourceforge.pmd.lang.apex.ast.ASTDottedExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTField;
import net.sourceforge.pmd.lang.apex.ast.ASTFieldDeclaration;
import net.sourceforge.pmd.lang.apex.ast.ASTMethodCallExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTModifierNode;
-import net.sourceforge.pmd.lang.apex.ast.ASTNewNameValueObjectExpression;
+import net.sourceforge.pmd.lang.apex.ast.ASTNewKeyValueObjectExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTReferenceExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTSoqlExpression;
import net.sourceforge.pmd.lang.apex.ast.ASTSoslExpression;
@@ -32,7 +31,7 @@
import apex.jorje.data.ast.TypeRef;
import apex.jorje.data.ast.TypeRef.ClassTypeRef;
import apex.jorje.semantic.ast.expression.MethodCallExpression;
-import apex.jorje.semantic.ast.expression.NewNameValueObjectExpression;
+import apex.jorje.semantic.ast.expression.NewKeyValueObjectExpression;
import apex.jorje.semantic.ast.expression.VariableExpression;
import apex.jorje.semantic.ast.member.Field;
import apex.jorje.semantic.ast.member.Parameter;
@@ -132,21 +131,17 @@ static boolean isMethodCallChain(final ASTMethodCallExpression methodNode, final
if (Helper.isMethodName(methodNode, methodName)) {
final ASTReferenceExpression reference = methodNode.getFirstChildOfType(ASTReferenceExpression.class);
if (reference != null) {
- final ASTDottedExpression dottedExpression = reference.getFirstChildOfType(ASTDottedExpression.class);
- if (dottedExpression != null) {
- final ASTMethodCallExpression nestedMethod = dottedExpression
- .getFirstChildOfType(ASTMethodCallExpression.class);
- if (nestedMethod != null) {
- String[] newMethodNames = Arrays.copyOf(methodNames, methodNames.length - 1);
- return isMethodCallChain(nestedMethod, newMethodNames);
- } else {
- String[] newClassName = Arrays.copyOf(methodNames, methodNames.length - 1);
- if (newClassName.length == 1) {
- return Helper.isMethodName(methodNode, newClassName[0], methodName);
- }
+ final ASTMethodCallExpression nestedMethod = reference
+ .getFirstChildOfType(ASTMethodCallExpression.class);
+ if (nestedMethod != null) {
+ String[] newMethodNames = Arrays.copyOf(methodNames, methodNames.length - 1);
+ return isMethodCallChain(nestedMethod, newMethodNames);
+ } else {
+ String[] newClassName = Arrays.copyOf(methodNames, methodNames.length - 1);
+ if (newClassName.length == 1) {
+ return Helper.isMethodName(methodNode, newClassName[0], methodName);
}
}
-
}
}
@@ -206,8 +201,8 @@ static String getFQVariableName(final ASTFieldDeclaration variable) {
return sb.toString();
}
- static String getFQVariableName(final ASTNewNameValueObjectExpression variable) {
- NewNameValueObjectExpression n = variable.getNode();
+ static String getFQVariableName(final ASTNewKeyValueObjectExpression variable) {
+ NewKeyValueObjectExpression n = variable.getNode();
String objType = "";
try {
// no other way to get this field, Apex Jorje does not expose it
diff --git a/pmd-apex/src/main/resources/rulesets/apex/braces.xml b/pmd-apex/src/main/resources/rulesets/apex/braces.xml
index 64a126c35..1e0dd4319 100644
--- a/pmd-apex/src/main/resources/rulesets/apex/braces.xml
+++ b/pmd-apex/src/main/resources/rulesets/apex/braces.xml
@@ -24,7 +24,7 @@ controlled from the rest.
@@ -59,7 +59,7 @@ controlled from the rest.
@@ -92,11 +92,9 @@ from the rest.
0]
|
-//ExpressionStatement[parent::IfElseBlockStatement]
-|
-//IfElseBlockStatement[parent::IfBlockStatement]
+//IfElseBlockStatement/BlockStatement[@CurlyBrace='false'][count(child::*) > 0]
]]>
@@ -131,11 +129,9 @@ from the rest.
diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/ast/ApexCompilerSoqlTest.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/ast/ApexCompilerSoqlTest.java
new file mode 100644
index 000000000..6a7c763e9
--- /dev/null
+++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/ast/ApexCompilerSoqlTest.java
@@ -0,0 +1,30 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.lang.apex.ast;
+
+import java.io.StringReader;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import net.sourceforge.pmd.lang.apex.ApexParserOptions;
+
+import apex.jorje.semantic.ast.compilation.Compilation;
+
+public class ApexCompilerSoqlTest {
+
+ private static final String CODE = "public class Foo {\n"
+ + " public List test1() {\n"
+ + " return Database.query(\'Select Id from Account LIMIT 100\');\n"
+ + " }\n"
+ + "}\n";
+
+ @Test
+ public void testSoqlCompilation() {
+ ApexParser parser = new ApexParser(new ApexParserOptions());
+ ApexNode cu = parser.parse(new StringReader(CODE));
+ Assert.assertNotNull(cu);
+ }
+}
diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/ast/ApexLexerTest.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/ast/ApexLexerTest.java
new file mode 100644
index 000000000..9f767b176
--- /dev/null
+++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/ast/ApexLexerTest.java
@@ -0,0 +1,48 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.lang.apex.ast;
+
+import org.antlr.runtime.ANTLRStringStream;
+import org.antlr.runtime.CharStream;
+import org.antlr.runtime.CommonTokenStream;
+import org.antlr.runtime.Token;
+import org.junit.Assert;
+import org.junit.Test;
+
+import apex.jorje.data.ast.CompilationUnit;
+import apex.jorje.parser.impl.ApexLexer;
+import apex.jorje.parser.impl.ApexParser;
+
+public class ApexLexerTest {
+
+ private static final String CODE = "public class Foo {\n"
+ + " public List test1() {\n"
+ + " return Database.query(\"Select Id from Account LIMIT 100\");\n"
+ + " }\n"
+ + "}\n";
+
+ @Test
+ public void testLexer() throws Exception {
+ CharStream in = new ANTLRStringStream(CODE);
+ ApexLexer lexer = new ApexLexer(in);
+
+ Token token = lexer.nextToken();
+ int tokenCount = 0;
+ while (token.getType() != Token.EOF) {
+ tokenCount++;
+ token = lexer.nextToken();
+ }
+ Assert.assertEquals(43, tokenCount);
+ }
+
+ @Test
+ public void testParser() throws Exception {
+ CharStream in = new ANTLRStringStream(CODE);
+ ApexLexer lexer = new ApexLexer(in);
+ ApexParser parser = new ApexParser(new CommonTokenStream(lexer));
+ CompilationUnit compilationUnit = parser.compilationUnit();
+ Assert.assertNotNull(compilationUnit);
+ }
+}
diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/ast/ApexParserTest.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/ast/ApexParserTest.java
index b65014a70..1e4498b82 100644
--- a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/ast/ApexParserTest.java
+++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/ast/ApexParserTest.java
@@ -121,7 +121,7 @@ public void parsesRealWorldClasses() throws Exception {
/**
* See bug #1485
*
- * @see #1485
+ * @see #1485 [apex] Analysis of some apex classes cause a stackoverflow error
*/
@Test
public void stackOverflowDuringClassParsing() throws Exception {
@@ -130,7 +130,7 @@ public void stackOverflowDuringClassParsing() throws Exception {
Assert.assertNotNull(rootNode);
int count = visitPosition(rootNode, 0);
- Assert.assertEquals(586, count);
+ Assert.assertEquals(489, count);
}
private int visitPosition(Node node, int count) {
diff --git a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/braces/xml/ForLoopsMustUseBraces.xml b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/braces/xml/ForLoopsMustUseBraces.xml
index 8354c4c2b..e71bbb5a8 100644
--- a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/braces/xml/ForLoopsMustUseBraces.xml
+++ b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/braces/xml/ForLoopsMustUseBraces.xml
@@ -31,7 +31,7 @@ public class Foo {
1
1
1
-
+ for-each, not ok
1
+
+
+ for-each, ok
+ 0
+
diff --git a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/complexity/xml/TooManyFields.xml b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/complexity/xml/TooManyFields.xml
index 4e0dee61e..6021b7356 100644
--- a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/complexity/xml/TooManyFields.xml
+++ b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/complexity/xml/TooManyFields.xml
@@ -82,11 +82,11 @@ public class Foo {
Integer a4;
Integer a5;
interface Bar {
- Integer a6;
- Integer a7;
- Integer a8;
- Integer a9;
- Integer a10;
+ static Integer a6;
+ static Integer a7;
+ static Integer a8;
+ static Integer a9;
+ static Integer a10;
}
}
]]>
diff --git a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/security/xml/ApexSharingViolations.xml b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/security/xml/ApexSharingViolations.xml
index 83777fd2a..fb70ffd08 100644
--- a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/security/xml/ApexSharingViolations.xml
+++ b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/security/xml/ApexSharingViolations.xml
@@ -49,7 +49,7 @@ public class Foo {
test1() {
- return Database.query("Select Id from Account LIMIT 100");
+ return Database.query('Select Id from Account LIMIT 100');
}
}
]]>
@@ -62,7 +62,7 @@ public class Foo {
test1() {
- return Database.query("Select Id from Account LIMIT 100");
+ return Database.query('Select Id from Account LIMIT 100');
}
}
]]>
diff --git a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/style/xml/VariableNamingConventions.xml b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/style/xml/VariableNamingConventions.xml
index 929c4c808..4a058a6ae 100644
--- a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/style/xml/VariableNamingConventions.xml
+++ b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/style/xml/VariableNamingConventions.xml
@@ -280,17 +280,7 @@ public class X {
}
]]>
-
-
- #1399 False positive for VariableNamingConventions with annotation @interface
- 0
-
-
-
+
Inner exception
0
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactory.java b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactory.java
index 79026d54a..599957bc3 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactory.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactory.java
@@ -567,6 +567,10 @@ private void parseSingleRuleNode(RuleSetReferenceId ruleSetReferenceId, RuleSetB
rule.setRuleSetName(ruleSetBuilder.getName());
rule.setExternalInfoUrl(ruleElement.getAttribute(EXTERNAL_INFO_URL));
+ if (hasAttributeSetTrue(ruleElement, "deprecated")) {
+ rule.setDeprecated(true);
+ }
+
if (hasAttributeSetTrue(ruleElement, "dfa")) {
rule.setUsesDFA();
}
diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryTest.java
index 3623e0137..4d2452a2f 100644
--- a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryTest.java
+++ b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryTest.java
@@ -431,6 +431,7 @@ public void testInvertedMinimumMaximumLanugageVersions() throws RuleSetNotFoundE
public void testDirectDeprecatedRule() throws RuleSetNotFoundException {
Rule r = loadFirstRule(DIRECT_DEPRECATED_RULE);
assertNotNull("Direct Deprecated Rule", r);
+ assertTrue(r.isDeprecated());
}
@Test
diff --git a/pmd-doc/pom.xml b/pmd-doc/pom.xml
new file mode 100644
index 000000000..23c78952a
--- /dev/null
+++ b/pmd-doc/pom.xml
@@ -0,0 +1,56 @@
+
+
+ 4.0.0
+ pmd-doc
+ PMD Documentation Generator
+ jar
+
+
+ net.sourceforge.pmd
+ pmd
+ 6.0.0-SNAPSHOT
+
+
+
+ 1.8
+ ${basedir}/../pmd-core
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 1.6.0
+
+
+
+ java
+
+ package
+
+
+
+ net.sourceforge.pmd.docs.GenerateRuleDocsCmd
+
+ ${project.basedir}
+
+
+
+
+
+
+
+
+ net.sourceforge.pmd
+ pmd-dist
+ ${project.version}
+
+
+
+ junit
+ junit
+ test
+
+
+
diff --git a/pmd-doc/src/main/java/net/sourceforge/pmd/docs/DefaultFileWriter.java b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/DefaultFileWriter.java
new file mode 100644
index 000000000..5c7e84ad2
--- /dev/null
+++ b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/DefaultFileWriter.java
@@ -0,0 +1,19 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.docs;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+public class DefaultFileWriter implements FileWriter {
+ @Override
+ public void write(Path path, List lines) throws IOException {
+ Files.createDirectories(path.getParent());
+ Files.write(path, lines, StandardCharsets.UTF_8);
+ }
+}
diff --git a/pmd-doc/src/main/java/net/sourceforge/pmd/docs/FileWriter.java b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/FileWriter.java
new file mode 100644
index 000000000..d47452754
--- /dev/null
+++ b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/FileWriter.java
@@ -0,0 +1,14 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.docs;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+
+public interface FileWriter {
+
+ void write(Path path, List lines) throws IOException;
+}
diff --git a/pmd-doc/src/main/java/net/sourceforge/pmd/docs/GenerateRuleDocsCmd.java b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/GenerateRuleDocsCmd.java
new file mode 100644
index 000000000..64f04bc96
--- /dev/null
+++ b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/GenerateRuleDocsCmd.java
@@ -0,0 +1,32 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.docs;
+
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.util.Iterator;
+
+import net.sourceforge.pmd.RuleSet;
+import net.sourceforge.pmd.RuleSetFactory;
+import net.sourceforge.pmd.RuleSetNotFoundException;
+
+public class GenerateRuleDocsCmd {
+ private GenerateRuleDocsCmd() {
+ // Utility class
+ }
+
+ public static void main(String[] args) throws RuleSetNotFoundException {
+ long start = System.currentTimeMillis();
+ Path output = FileSystems.getDefault().getPath(args[0]).resolve("..").toAbsolutePath().normalize();
+ System.out.println("Generating docs into " + output);
+ RuleDocGenerator generator = new RuleDocGenerator(new DefaultFileWriter(), output);
+
+ RuleSetFactory ruleSetFactory = new RuleSetFactory();
+ Iterator registeredRuleSets = ruleSetFactory.getRegisteredRuleSets();
+
+ generator.generate(registeredRuleSets);
+ System.out.println("Generated docs in " + (System.currentTimeMillis() - start) + " ms");
+ }
+}
diff --git a/pmd-doc/src/main/java/net/sourceforge/pmd/docs/RuleDocGenerator.java b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/RuleDocGenerator.java
new file mode 100644
index 000000000..5eb70b5fa
--- /dev/null
+++ b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/RuleDocGenerator.java
@@ -0,0 +1,418 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.docs;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import net.sourceforge.pmd.PropertyDescriptor;
+import net.sourceforge.pmd.Rule;
+import net.sourceforge.pmd.RuleSet;
+import net.sourceforge.pmd.RuleSetNotFoundException;
+import net.sourceforge.pmd.lang.Language;
+import net.sourceforge.pmd.lang.rule.RuleReference;
+import net.sourceforge.pmd.lang.rule.XPathRule;
+
+public class RuleDocGenerator {
+ private static final String LANGUAGE_INDEX_FILENAME_PATTERN = "docs/pages/pmd/rules/${language.tersename}.md";
+ private static final String LANGUAGE_INDEX_PERMALINK_PATTERN = "pmd_rules_${language.tersename}.html";
+ private static final String RULESET_INDEX_FILENAME_PATTERN = "docs/pages/pmd/rules/${language.tersename}/${ruleset.name}.md";
+ private static final String RULESET_INDEX_PERMALINK_PATTERN = "pmd_rules_${language.tersename}_${ruleset.name}.html";
+
+ private static final String DEPRECATION_LABEL_SMALL = "Deprecated ";
+ private static final String DEPRECATION_LABEL = "Deprecated ";
+
+ private static final String GITHUB_SOURCE_LINK = "https://github.com/pmd/pmd/blob/master/";
+
+ private final Path root;
+ private final FileWriter writer;
+
+ public RuleDocGenerator(FileWriter writer, Path root) {
+ this.root = Objects.requireNonNull(root, "Root directory must be provided");
+ this.writer = Objects.requireNonNull(writer, "A file writer must be provided");
+
+ Path docsDir = root.resolve("docs");
+ if (!Files.exists(docsDir) || !Files.isDirectory(docsDir)) {
+ throw new IllegalArgumentException("Couldn't find \"docs\" subdirectory");
+ }
+ }
+
+ public void generate(Iterator rulesets) {
+ Map> sortedRulesets;
+ try {
+ sortedRulesets = sortRulesets(rulesets);
+ generateLanguageIndex(sortedRulesets);
+ generateRuleSetIndex(sortedRulesets);
+
+ } catch (RuleSetNotFoundException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Path getAbsoluteOutputPath(String filename) {
+ return root.resolve(FilenameUtils.normalize(filename));
+ }
+
+ private Map> sortRulesets(Iterator rulesets) throws RuleSetNotFoundException {
+ Map> rulesetsByLanguage = new HashMap<>();
+
+ while (rulesets.hasNext()) {
+ RuleSet ruleset = rulesets.next();
+ Language language = getRuleSetLanguage(ruleset);
+
+ if (!rulesetsByLanguage.containsKey(language)) {
+ rulesetsByLanguage.put(language, new ArrayList());
+ }
+ rulesetsByLanguage.get(language).add(ruleset);
+ }
+
+ for (List rulesetsOfOneLanguage : rulesetsByLanguage.values()) {
+ Collections.sort(rulesetsOfOneLanguage, new Comparator() {
+ @Override
+ public int compare(RuleSet o1, RuleSet o2) {
+ return o1.getName().compareToIgnoreCase(o2.getName());
+ }
+ });
+ }
+ return rulesetsByLanguage;
+ }
+
+ /**
+ * Rulesets could potentially contain rules from various languages.
+ * But for built-in rulesets, all rules within one ruleset belong to
+ * one language. So we take the language of the first rule.
+ * @param ruleset
+ * @return the terse name of the ruleset's language
+ */
+ private static Language getRuleSetLanguage(RuleSet ruleset) {
+ Collection rules = ruleset.getRules();
+ if (rules.isEmpty()) {
+ throw new RuntimeException("Ruleset " + ruleset.getFileName() + " is empty!");
+ }
+ return rules.iterator().next().getLanguage();
+ }
+
+ /**
+ * Writes for each language an index file, which lists the rulesets, the rules
+ * and links to the ruleset pages.
+ * @param rulesets all rulesets
+ * @throws IOException
+ */
+ private void generateLanguageIndex(Map> rulesets) throws IOException {
+ for (Map.Entry> entry : rulesets.entrySet()) {
+ String languageTersename = entry.getKey().getTerseName();
+ String filename = LANGUAGE_INDEX_FILENAME_PATTERN
+ .replace("${language.tersename}", languageTersename);
+ Path path = getAbsoluteOutputPath(filename);
+
+ List lines = new LinkedList<>();
+ lines.add("---");
+ lines.add("title: " + entry.getKey().getName() + " Rules");
+ lines.add("permalink: " + LANGUAGE_INDEX_PERMALINK_PATTERN.replace("${language.tersename}", languageTersename));
+ lines.add("folder: pmd/rules");
+ lines.add("---");
+
+ lines.add("List of rulesets and rules contained in each ruleset.");
+ lines.add("");
+
+ for (RuleSet ruleset : entry.getValue()) {
+ String link = RULESET_INDEX_PERMALINK_PATTERN
+ .replace("${language.tersename}", languageTersename)
+ .replace("${ruleset.name}", getRuleSetFilename(ruleset));
+ lines.add("* [" + ruleset.getName() + "](" + link + "): " + getRuleSetDescriptionSingleLine(ruleset));
+ }
+ lines.add("");
+
+ for (RuleSet ruleset : entry.getValue()) {
+ lines.add("## " + ruleset.getName());
+
+ for (Rule rule : getSortedRules(ruleset)) {
+ String link = RULESET_INDEX_PERMALINK_PATTERN
+ .replace("${language.tersename}", languageTersename)
+ .replace("${ruleset.name}", getRuleSetFilename(ruleset));
+ if (rule instanceof RuleReference) {
+ RuleReference ref = (RuleReference) rule;
+ if (ruleset.getFileName().equals(ref.getRuleSetReference().getRuleSetFileName())) {
+ // rule renamed within same ruleset
+ lines.add("* [" + rule.getName() + "](" + link + "#" + rule.getName().toLowerCase(Locale.ROOT) + "): "
+ + DEPRECATION_LABEL_SMALL
+ + "The rule has been renamed. Use instead "
+ + "[" + ref.getRule().getName() + "](" + link + "#" + ref.getRule().getName().toLowerCase(Locale.ROOT) + ").");
+ } else {
+ // rule moved to another ruleset...
+ String otherLink = RULESET_INDEX_PERMALINK_PATTERN
+ .replace("${language.tersename}", languageTersename)
+ .replace("${ruleset.name}", getRuleSetFilename(ref.getRuleSetReference().getRuleSetFileName()));
+ lines.add("* [" + rule.getName() + "](" + link + "#" + rule.getName().toLowerCase(Locale.ROOT) + "): "
+ + DEPRECATION_LABEL_SMALL
+ + "The rule has been moved to another ruleset. Use instead "
+ + "[" + ref.getRule().getName() + "](" + otherLink + "#" + ref.getRule().getName().toLowerCase(Locale.ROOT) + ").");
+ }
+ } else {
+ link += "#" + rule.getName().toLowerCase(Locale.ROOT);
+ lines.add("* [" + rule.getName() + "](" + link + "): "
+ + (rule.isDeprecated() ? DEPRECATION_LABEL_SMALL : "")
+ + getShortRuleDescription(rule));
+ }
+ }
+ lines.add("");
+ }
+
+ System.out.println("Generated " + path);
+ writer.write(path, lines);
+ }
+ }
+
+ /**
+ * Shortens and escapes (for markdown) some special characters. Otherwise the shortened text
+ * could contain some unfinished sequences.
+ * @param rule
+ * @return
+ */
+ private static String getShortRuleDescription(Rule rule) {
+ return StringUtils.abbreviate(
+ StringUtils.stripToEmpty(rule.getDescription().replaceAll("\n|\r", "")
+ .replaceAll("\\|", "\\\\|")
+ .replaceAll("`", "'")
+ .replaceAll("\\*", "")), 100);
+ }
+
+ /**
+ * Gets the sanitized base name of the ruleset.
+ * For some reason, the filename might contain some newlines, which are removed.
+ * @param ruleset
+ * @return
+ */
+ private static String getRuleSetFilename(RuleSet ruleset) {
+ return getRuleSetFilename(ruleset.getFileName());
+ }
+
+ private static String getRuleSetFilename(String rulesetFileName) {
+ return FilenameUtils.getBaseName(StringUtils.chomp(rulesetFileName));
+ }
+
+ private static String getRuleSetDescriptionSingleLine(RuleSet ruleset) {
+ String description = ruleset.getDescription();
+ description = description.replaceAll("\\n|\\r", " ");
+ description = StringUtils.stripToEmpty(description);
+ return description;
+ }
+
+ /**
+ * Generates for each ruleset a page. The page contains the details for each rule.
+ *
+ * @param rulesets all rulesets
+ * @throws IOException
+ */
+ private void generateRuleSetIndex(Map> rulesets) throws IOException {
+ for (Map.Entry> entry : rulesets.entrySet()) {
+ String languageTersename = entry.getKey().getTerseName();
+ for (RuleSet ruleset : entry.getValue()) {
+ String filename = RULESET_INDEX_FILENAME_PATTERN
+ .replace("${language.tersename}", languageTersename)
+ .replace("${ruleset.name}", getRuleSetFilename(ruleset));
+
+ Path path = getAbsoluteOutputPath(filename);
+
+ String permalink = RULESET_INDEX_PERMALINK_PATTERN
+ .replace("${language.tersename}", languageTersename)
+ .replace("${ruleset.name}", getRuleSetFilename(ruleset));
+
+ List lines = new LinkedList<>();
+ lines.add("---");
+ lines.add("title: " + ruleset.getName());
+ lines.add("summary: " + getRuleSetDescriptionSingleLine(ruleset));
+ lines.add("permalink: " + permalink);
+ lines.add("folder: pmd/rules/" + languageTersename);
+ lines.add("sidebaractiveurl: /" + LANGUAGE_INDEX_PERMALINK_PATTERN.replace("${language.tersename}", languageTersename));
+ lines.add("editmepath: ../" + getRuleSetSourceFilepath(ruleset));
+ lines.add("---");
+
+ for (Rule rule : getSortedRules(ruleset)) {
+ lines.add("## " + rule.getName());
+ lines.add("");
+
+ if (rule instanceof RuleReference) {
+ RuleReference ref = (RuleReference) rule;
+ if (ruleset.getFileName().equals(ref.getRuleSetReference().getRuleSetFileName())) {
+ // rule renamed within same ruleset
+ lines.add(DEPRECATION_LABEL);
+ lines.add("");
+ lines.add("This rule has been renamed. Use instead: ["
+ + ref.getRule().getName() + "](" + "#" + ref.getRule().getName().toLowerCase(Locale.ROOT) + ")");
+ lines.add("");
+ } else {
+ // rule moved to another ruleset
+ String otherLink = RULESET_INDEX_PERMALINK_PATTERN
+ .replace("${language.tersename}", languageTersename)
+ .replace("${ruleset.name}", getRuleSetFilename(ref.getRuleSetReference().getRuleSetFileName()));
+ lines.add(DEPRECATION_LABEL);
+ lines.add("");
+ lines.add("The rule has been moved to another ruleset. Use instead: ["
+ + ref.getRule().getName() + "](" + otherLink + "#" + ref.getRule().getName().toLowerCase(Locale.ROOT) + ")");
+ lines.add("");
+ }
+ }
+
+ if (rule.isDeprecated()) {
+ lines.add(DEPRECATION_LABEL);
+ lines.add("");
+ }
+ if (rule.getSince() != null) {
+ lines.add("**Since:** PMD " + rule.getSince());
+ lines.add("");
+ }
+ lines.add("**Priority:** " + rule.getPriority() + " (" + rule.getPriority().getPriority() + ")");
+ lines.add("");
+
+ if (rule.getMinimumLanguageVersion() != null) {
+ lines.add("**Minimum Language Version:** "
+ + rule.getLanguage().getName() + " " + rule.getMinimumLanguageVersion().getVersion());
+ lines.add("");
+ }
+
+ lines.add(StringUtils.stripToEmpty(rule.getDescription()));
+ lines.add("");
+
+ if (rule instanceof XPathRule || rule instanceof RuleReference && ((RuleReference) rule).getRule() instanceof XPathRule) {
+ lines.add("```");
+ lines.add(StringUtils.stripToEmpty(rule.getProperty(XPathRule.XPATH_DESCRIPTOR)));
+ lines.add("```");
+ lines.add("");
+ } else {
+ lines.add("**This rule is defined by the following Java class:** "
+ + "[" + rule.getRuleClass() + "]("
+ + GITHUB_SOURCE_LINK + getRuleClassSourceFilepath(rule.getRuleClass())
+ + ")");
+ lines.add("");
+ }
+
+ if (!rule.getExamples().isEmpty()) {
+ lines.add("**Example(s):**");
+ lines.add("");
+ for (String example : rule.getExamples()) {
+ lines.add("```");
+ lines.add(StringUtils.stripToEmpty(example));
+ lines.add("```");
+ lines.add("");
+ }
+ }
+
+ List> properties = new ArrayList<>(rule.getPropertyDescriptors());
+ // filter out standard properties
+ properties.remove(Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR);
+ properties.remove(Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR);
+ properties.remove(XPathRule.XPATH_DESCRIPTOR);
+ properties.remove(XPathRule.VERSION_DESCRIPTOR);
+
+ if (!properties.isEmpty()) {
+ lines.add("**This rule has the following properties:**");
+ lines.add("");
+ lines.add("|Name|Default Value|Description|");
+ lines.add("|----|-------------|-----------|");
+ for (PropertyDescriptor> propertyDescriptor : properties) {
+ lines.add("|" + propertyDescriptor.name()
+ + "|" + (propertyDescriptor.defaultValue() != null ? String.valueOf(propertyDescriptor.defaultValue()) : "")
+ + "|" + propertyDescriptor.description()
+ + "|");
+ }
+ lines.add("");
+ }
+ }
+
+ writer.write(path, lines);
+ System.out.println("Generated " + path);
+ }
+ }
+ }
+
+ private List getSortedRules(RuleSet ruleset) {
+ List sortedRules = new ArrayList<>(ruleset.getRules());
+ Collections.sort(sortedRules, new Comparator() {
+ @Override
+ public int compare(Rule o1, Rule o2) {
+ return o1.getName().compareToIgnoreCase(o2.getName());
+ }
+ });
+ return sortedRules;
+ }
+
+ /**
+ * Searches for the source file of the given ruleset. This provides the information
+ * for the "editme" link.
+ *
+ * @param ruleset the ruleset to search for.
+ * @return
+ * @throws IOException
+ */
+ private String getRuleSetSourceFilepath(RuleSet ruleset) throws IOException {
+ final String rulesetFilename = FilenameUtils.normalize(StringUtils.chomp(ruleset.getFileName()));
+ final List foundPathResult = new LinkedList<>();
+
+ Files.walkFileTree(root, new SimpleFileVisitor() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ String path = file.toString();
+ if (path.contains("src") && path.endsWith(rulesetFilename)) {
+ foundPathResult.add(file);
+ return FileVisitResult.TERMINATE;
+ }
+ return super.visitFile(file, attrs);
+ }
+ });
+
+ if (!foundPathResult.isEmpty()) {
+ Path foundPath = foundPathResult.get(0);
+ foundPath = root.relativize(foundPath);
+ return foundPath.toString();
+ }
+
+ return StringUtils.chomp(ruleset.getFileName());
+ }
+
+ private String getRuleClassSourceFilepath(String ruleClass) throws IOException {
+ final String relativeSourceFilename = ruleClass.replaceAll("\\.", File.separator) + ".java";
+ final List foundPathResult = new LinkedList<>();
+
+ Files.walkFileTree(root, new SimpleFileVisitor() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ String path = file.toString();
+ if (path.contains("src") && path.endsWith(relativeSourceFilename)) {
+ foundPathResult.add(file);
+ return FileVisitResult.TERMINATE;
+ }
+ return super.visitFile(file, attrs);
+ }
+ });
+
+ if (!foundPathResult.isEmpty()) {
+ Path foundPath = foundPathResult.get(0);
+ foundPath = root.relativize(foundPath);
+ return FilenameUtils.normalize(foundPath.toString(), true);
+ }
+
+ return FilenameUtils.normalize(relativeSourceFilename, true);
+ }
+}
diff --git a/pmd-doc/src/test/java/net/sourceforge/pmd/docs/MockedFileWriter.java b/pmd-doc/src/test/java/net/sourceforge/pmd/docs/MockedFileWriter.java
new file mode 100644
index 000000000..a533e136f
--- /dev/null
+++ b/pmd-doc/src/test/java/net/sourceforge/pmd/docs/MockedFileWriter.java
@@ -0,0 +1,46 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.docs;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+
+public class MockedFileWriter implements FileWriter {
+
+ public static class FileEntry {
+ private String filename;
+ private String content;
+
+ public String getFilename() {
+ return filename;
+ }
+
+ public String getContent() {
+ return content;
+ }
+ }
+
+ private List data = new ArrayList<>();
+
+ @Override
+ public void write(Path path, List lines) throws IOException {
+ FileEntry entry = new FileEntry();
+ entry.filename = path.toString();
+ entry.content = StringUtils.join(lines, System.getProperty("line.separator"));
+ data.add(entry);
+ }
+
+ public List getData() {
+ return data;
+ }
+
+ public void reset() {
+ data.clear();
+ }
+}
diff --git a/pmd-doc/src/test/java/net/sourceforge/pmd/docs/RuleDocGeneratorTest.java b/pmd-doc/src/test/java/net/sourceforge/pmd/docs/RuleDocGeneratorTest.java
new file mode 100644
index 000000000..43e64bb1d
--- /dev/null
+++ b/pmd-doc/src/test/java/net/sourceforge/pmd/docs/RuleDocGeneratorTest.java
@@ -0,0 +1,64 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.docs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+
+import org.apache.commons.io.IOUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import net.sourceforge.pmd.RuleSet;
+import net.sourceforge.pmd.RuleSetFactory;
+import net.sourceforge.pmd.RuleSetNotFoundException;
+import net.sourceforge.pmd.docs.MockedFileWriter.FileEntry;
+
+public class RuleDocGeneratorTest {
+
+ private MockedFileWriter writer = new MockedFileWriter();
+ private Path root;
+
+ @Before
+ public void setup() throws IOException {
+ writer.reset();
+
+ root = Files.createTempDirectory("pmd-ruledocgenerator-test");
+ Files.createDirectory(root.resolve("docs"));
+ }
+
+ @After
+ public void cleanup() throws IOException {
+ Files.delete(root.resolve("docs"));
+ Files.delete(root);
+ }
+
+ @Test
+ public void testSingleRuleset() throws RuleSetNotFoundException, IOException {
+ RuleDocGenerator generator = new RuleDocGenerator(writer, root);
+
+ RuleSetFactory rsf = new RuleSetFactory();
+ RuleSet ruleset = rsf.createRuleSet("rulesets/ruledoctest/sample.xml");
+
+ generator.generate(Arrays.asList(ruleset).iterator());
+
+ assertEquals(2, writer.getData().size());
+ FileEntry languageIndex = writer.getData().get(0);
+ assertTrue(languageIndex.getFilename().endsWith("docs/pages/pmd/rules/java.md"));
+ assertEquals(IOUtils.toString(RuleDocGeneratorTest.class.getResourceAsStream("/expected/java.md")),
+ languageIndex.getContent());
+
+ FileEntry ruleSetIndex = writer.getData().get(1);
+ assertTrue(ruleSetIndex.getFilename().endsWith("docs/pages/pmd/rules/java/sample.md"));
+ assertEquals(IOUtils.toString(RuleDocGeneratorTest.class.getResourceAsStream("/expected/sample.md")),
+ ruleSetIndex.getContent());
+ }
+}
diff --git a/pmd-doc/src/test/resources/expected/java.md b/pmd-doc/src/test/resources/expected/java.md
new file mode 100644
index 000000000..8ddbfc08d
--- /dev/null
+++ b/pmd-doc/src/test/resources/expected/java.md
@@ -0,0 +1,15 @@
+---
+title: Java Rules
+permalink: pmd_rules_java.html
+folder: pmd/rules
+---
+List of rulesets and rules contained in each ruleset.
+
+* [Sample](pmd_rules_java_sample.html): Sample ruleset to test rule doc generation.
+
+## Sample
+* [DeprecatedSample](pmd_rules_java_sample.html#deprecatedsample): Deprecated Just some description of a deprecated rule.
+* [JumbledIncrementer](pmd_rules_java_sample.html#jumbledincrementer): Avoid jumbled loop incrementers - its usually a mistake, and is confusing even if intentional.
+* [MovedRule](pmd_rules_java_sample.html#movedrule): Deprecated The rule has been moved to another ruleset. Use instead [JumbledIncrementer](pmd_rules_java_basic.html#jumbledincrementer).
+* [OverrideBothEqualsAndHashcode](pmd_rules_java_sample.html#overridebothequalsandhashcode): Override both 'public boolean Object.equals(Object other)', and 'public int Object.hashCode()', o...
+* [RenamedRule](pmd_rules_java_sample.html#renamedrule): Deprecated The rule has been renamed. Use instead [JumbledIncrementer](pmd_rules_java_sample.html#jumbledincrementer).
diff --git a/pmd-doc/src/test/resources/expected/sample.md b/pmd-doc/src/test/resources/expected/sample.md
new file mode 100644
index 000000000..72a9d3cf9
--- /dev/null
+++ b/pmd-doc/src/test/resources/expected/sample.md
@@ -0,0 +1,173 @@
+---
+title: Sample
+summary: Sample ruleset to test rule doc generation.
+permalink: pmd_rules_java_sample.html
+folder: pmd/rules/java
+sidebaractiveurl: /pmd_rules_java.html
+editmepath: ../rulesets/ruledoctest/sample.xml
+---
+## DeprecatedSample
+
+Deprecated
+
+**Since:** PMD 1.0
+
+**Priority:** Medium (3)
+
+Just some description of a deprecated rule.
+
+```
+//ForStatement
+```
+
+## JumbledIncrementer
+
+**Since:** PMD 1.0
+
+**Priority:** Medium (3)
+
+Avoid jumbled loop incrementers - its usually a mistake, and is confusing even if intentional.
+
+```
+//ForStatement
+ [
+ ForUpdate/StatementExpressionList/StatementExpression/PostfixExpression/PrimaryExpression/PrimaryPrefix/Name/@Image
+ =
+ ancestor::ForStatement/ForInit//VariableDeclaratorId/@Image
+ ]
+```
+
+**Example(s):**
+
+```
+public class JumbledIncrementerRule1 {
+ public void foo() {
+ for (int i = 0; i < 10; i++) { // only references 'i'
+ for (int k = 0; k < 20; i++) { // references both 'i' and 'k'
+ System.out.println("Hello");
+ }
+ }
+ }
+}
+```
+
+**This rule has the following properties:**
+
+|Name|Default Value|Description|
+|----|-------------|-----------|
+|sampleAdditionalProperty|the value|This is a additional property for tests|
+
+## MovedRule
+
+Deprecated
+
+The rule has been moved to another ruleset. Use instead: [JumbledIncrementer](pmd_rules_java_basic.html#jumbledincrementer)
+
+**Since:** PMD 1.0
+
+**Priority:** Medium (3)
+
+Avoid jumbled loop incrementers - its usually a mistake, and is confusing even if intentional.
+
+```
+//ForStatement
+ [
+ ForUpdate/StatementExpressionList/StatementExpression/PostfixExpression/PrimaryExpression/PrimaryPrefix/Name/@Image
+ =
+ ancestor::ForStatement/ForInit//VariableDeclaratorId/@Image
+ ]
+```
+
+**Example(s):**
+
+```
+public class JumbledIncrementerRule1 {
+ public void foo() {
+ for (int i = 0; i < 10; i++) { // only references 'i'
+ for (int k = 0; k < 20; i++) { // references both 'i' and 'k'
+ System.out.println("Hello");
+ }
+ }
+ }
+}
+```
+
+## OverrideBothEqualsAndHashcode
+
+**Since:** PMD 0.4
+
+**Priority:** Medium (3)
+
+**Minimum Language Version:** Java 1.5
+
+Override both `public boolean Object.equals(Object other)`, and `public int Object.hashCode()`, or override neither.
+Even if you are inheriting a `hashCode()` from a parent class, consider implementing hashCode and explicitly
+delegating to your superclass.
+
+**This rule is defined by the following Java class:** [net.sourceforge.pmd.lang.java.rule.basic.OverrideBothEqualsAndHashcodeRule](https://github.com/pmd/pmd/blob/master/net/sourceforge/pmd/lang/java/rule/basic/OverrideBothEqualsAndHashcodeRule.java)
+
+**Example(s):**
+
+```
+public class Bar { // poor, missing a hashcode() method
+ public boolean equals(Object o) {
+ // do some comparison
+ }
+}
+
+public class Baz { // poor, missing an equals() method
+ public int hashCode() {
+ // return some hash value
+ }
+}
+
+public class Foo { // perfect, both methods provided
+ public boolean equals(Object other) {
+ // do some comparison
+ }
+ public int hashCode() {
+ // return some hash value
+ }
+}
+```
+
+## RenamedRule
+
+Deprecated
+
+This rule has been renamed. Use instead: [JumbledIncrementer](#jumbledincrementer)
+
+**Since:** PMD 1.0
+
+**Priority:** Medium (3)
+
+Avoid jumbled loop incrementers - its usually a mistake, and is confusing even if intentional.
+
+```
+//ForStatement
+ [
+ ForUpdate/StatementExpressionList/StatementExpression/PostfixExpression/PrimaryExpression/PrimaryPrefix/Name/@Image
+ =
+ ancestor::ForStatement/ForInit//VariableDeclaratorId/@Image
+ ]
+```
+
+**Example(s):**
+
+```
+public class JumbledIncrementerRule1 {
+ public void foo() {
+ for (int i = 0; i < 10; i++) { // only references 'i'
+ for (int k = 0; k < 20; i++) { // references both 'i' and 'k'
+ System.out.println("Hello");
+ }
+ }
+ }
+}
+```
+
+**This rule has the following properties:**
+
+|Name|Default Value|Description|
+|----|-------------|-----------|
+|sampleAdditionalProperty|the value|This is a additional property for tests|
diff --git a/pmd-doc/src/test/resources/rulesets/ruledoctest/sample.xml b/pmd-doc/src/test/resources/rulesets/ruledoctest/sample.xml
new file mode 100644
index 000000000..2f3b5c876
--- /dev/null
+++ b/pmd-doc/src/test/resources/rulesets/ruledoctest/sample.xml
@@ -0,0 +1,116 @@
+
+
+
+
+Sample ruleset to test rule doc generation.
+
+
+
+
+Override both `public boolean Object.equals(Object other)`, and `public int Object.hashCode()`, or override neither.
+Even if you are inheriting a `hashCode()` from a parent class, consider implementing hashCode and explicitly
+delegating to your superclass.
+
+ 3
+
+
+
+
+
+
+
+Avoid jumbled loop incrementers - its usually a mistake, and is confusing even if intentional.
+
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Just some description of a deprecated rule.
+
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTType.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTType.java
index b398ca9a7..bdd26c4f5 100644
--- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTType.java
+++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTType.java
@@ -23,11 +23,11 @@ public Object jjtAccept(JavaParserVisitor visitor, Object data) {
}
public String getTypeImage() {
- ASTPrimitiveType prim = getFirstDescendantOfType(ASTPrimitiveType.class);
- if (prim != null) {
- return prim.getImage();
+ ASTClassOrInterfaceType refType = getFirstDescendantOfType(ASTClassOrInterfaceType.class);
+ if (refType != null) {
+ return refType.getImage();
}
- return getFirstDescendantOfType(ASTClassOrInterfaceType.class).getImage();
+ return getFirstDescendantOfType(ASTPrimitiveType.class).getImage();
}
public int getArrayDepth() {
diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/impl/visitors/DefaultNcssVisitor.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/impl/visitors/DefaultNcssVisitor.java
index 082e55cea..79da9a5e2 100644
--- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/impl/visitors/DefaultNcssVisitor.java
+++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/impl/visitors/DefaultNcssVisitor.java
@@ -7,6 +7,7 @@
import org.apache.commons.lang3.mutable.MutableInt;
import net.sourceforge.pmd.lang.java.ast.ASTAnnotationTypeDeclaration;
+import net.sourceforge.pmd.lang.java.ast.ASTAssertStatement;
import net.sourceforge.pmd.lang.java.ast.ASTBreakStatement;
import net.sourceforge.pmd.lang.java.ast.ASTCatchStatement;
import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration;
@@ -220,4 +221,10 @@ public Object visit(ASTInitializer node, Object data) {
return super.visit(node, data);
}
+ @Override
+ public Object visit(ASTAssertStatement node, Object data) {
+ ((MutableInt) data).increment();
+ return super.visit(node, data);
+ }
+
}
diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/comments/CommentDefaultAccessModifierRule.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/comments/CommentDefaultAccessModifierRule.java
index a219be226..9ea78854e 100644
--- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/comments/CommentDefaultAccessModifierRule.java
+++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/comments/CommentDefaultAccessModifierRule.java
@@ -12,6 +12,7 @@
import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceBodyDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
+import net.sourceforge.pmd.lang.java.ast.ASTConstructorDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTFieldDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclarator;
@@ -86,6 +87,14 @@ public Object visit(final ASTClassOrInterfaceDeclaration decl, final Object data
return super.visit(decl, data);
}
+ @Override
+ public Object visit(final ASTConstructorDeclaration decl, Object data) {
+ if (shouldReport(decl)) {
+ addViolationWithMessage(data, decl, String.format(MESSAGE, decl.getImage(), "constructor"));
+ }
+ return super.visit(decl, data);
+ }
+
private boolean shouldReport(final AbstractJavaAccessNode decl) {
List parentClassOrInterface = decl
.getParentsOfType(ASTClassOrInterfaceDeclaration.class);
diff --git a/pmd-java/src/main/resources/rulesets/java/basic.xml b/pmd-java/src/main/resources/rulesets/java/basic.xml
index f7f06a0ad..df103cd67 100644
--- a/pmd-java/src/main/resources/rulesets/java/basic.xml
+++ b/pmd-java/src/main/resources/rulesets/java/basic.xml
@@ -35,13 +35,13 @@ Avoid jumbled loop incrementers - its usually a mistake, and is confusing even i
diff --git a/pmd-java/src/main/resources/rulesets/java/comments.xml b/pmd-java/src/main/resources/rulesets/java/comments.xml
index 9462a4862..42dad25e1 100755
--- a/pmd-java/src/main/resources/rulesets/java/comments.xml
+++ b/pmd-java/src/main/resources/rulesets/java/comments.xml
@@ -82,9 +82,9 @@ A rule for the politically correct... we don't want to offend anyone.
message="Missing commented default access modifier"
externalInfoUrl="${pmd.website.baseurl}/rules/java/comments.html#CommentDefaultAccessModifier">
- To avoid mistakes if we want that a Method, Field or Nested class have a default access modifier
- we must add a comment at the beginning of the Method, Field or Nested class.
- By default the comment must be /* default */, if you want another, you have to provide a regex.
+ To avoid mistakes if we want that a Method, Constructor, Field or Nested class have a default access modifier
+ we must add a comment at the beginning of it's declaration.
+ By default the comment must be /* default */, if you want another, you have to provide a regexp.
3
diff --git a/pmd-java/src/main/resources/rulesets/java/metrics.xml b/pmd-java/src/main/resources/rulesets/java/metrics.xml
index 422a7e56f..5a35fb5bb 100644
--- a/pmd-java/src/main/resources/rulesets/java/metrics.xml
+++ b/pmd-java/src/main/resources/rulesets/java/metrics.xml
@@ -15,52 +15,47 @@
class="net.sourceforge.pmd.lang.java.metrics.rule.CyclomaticComplexityRule"
metrics="true"
externalInfoUrl="${pmd.website.baseurl}/rules/java/codesize.html#CyclomaticComplexity">
-
-
+
3
@@ -71,24 +66,38 @@ public class Foo { // This has a Cyclomatic Complexity = 12
since="3.9"
class="net.sourceforge.pmd.lang.java.metrics.rule.NcssCountRule"
metrics="true"
- externalInfoUrl="${pmd.website.baseurl}/rules/java/codesize.html#NcssTypeCount">
+ externalInfoUrl="${pmd.website.baseurl}/rules/java/codesize.html#NcssCount">
- This rule uses the NCSS (Non-Commenting Source Statements) algorithm to determine the number of lines
- of code for a given type. NCSS ignores comments, and counts actual statements. Using this algorithm,
- lines of code that are split are counted as one.
+ This rule uses the NCSS (Non-Commenting Source Statements) metric to determine the number of lines
+ of code in a class, method or constructor. NCSS ignores comments, blank lines, and only counts actual
+ statements. For more details on the calculation, see the documentation ofthe[NCSSmetric](/pmd_java_metrics_index.html#non-commenting-source-statements-ncss).
3
@@ -103,41 +112,51 @@ public class Foo extends Bar {
externalInfoUrl="${pmd.website.baseurl}/rules/java/codesize.html#NPathComplexity">
The NPath complexity of a method is the number of acyclic execution paths through that method.
+ While cyclomatic complexity counts the number of decision points in a method, NPath counts the number of
+ full paths from the beginning to the end of the block of the method. That metric grows exponentially, as
+ it multiplies the complexity of statements in the same block. For more details on the calculation, see the
+ documentation of the [NPath metric](/pmd_java_metrics_index.html#npath-complexity-npath).
+
A threshold of 200 is generally considered the point where measures should be taken to reduce
complexity and increase readability.
3
r) {
- doSomething();
- while (f < 5 ) {
- anotherThing();
- f -= 27;
- }
- } else {
- tryThis();
- }
+public class Foo {
+ public static void bar() { // Ncss = 252: reported!
+ boolean a, b = true;
+ try { // 2 * 2 + 2 = 6
+ if (true) { // 2
+ List buz = new ArrayList();
+ }
+
+ for(int i = 0; i < 19; i++) { // * 2
+ List buz = new ArrayList();
+ }
+ } catch(Exception e) {
+ if (true) { // 2
+ e.printStackTrace();
}
}
- if ( r - n > 45) {
- while (doMagic()) {
- findRabbits();
- }
+
+ while (j++ < 20) { // * 2
+ List buz = new ArrayList();
}
- try {
- doSomethingDangerous();
- } catch (Exception ex) {
- makeAmends();
- } finally {
- dontDoItAgain();
+
+ switch(j) { // * 7
+ case 1:
+ case 2: break;
+ case 3: j = 5; break;
+ case 4: if (b && a) { bar(); } break;
+ default: break;
}
+
+ do { // * 3
+ List buz = new ArrayList();
+ } while (a && j++ < 30);
}
}
-
]]>
diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/metrics/impl/xml/NcssTest.xml b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/metrics/impl/xml/NcssTest.xml
index ecf84267f..80c26d932 100644
--- a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/metrics/impl/xml/NcssTest.xml
+++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/metrics/impl/xml/NcssTest.xml
@@ -104,6 +104,7 @@
e.printStackTrace();
} catch (ThemAll pokemon) {
pokemon.train();
+ assert pokemon.level > 12;
} finally {
// Do nothing
}
@@ -125,11 +126,11 @@
true
5
- 'com.company.money.Foo' has value 65 highest 20.
+ 'com.company.money.Foo' has value 66 highest 21.
'com.company.money.Foo#Foo()' has value 2.
'com.company.money.Foo#Foo(int)' has value 13.
'com.company.money.Foo#foo()' has value 14.
- 'com.company.money.Foo#main(String)' has value 20.
+ 'com.company.money.Foo#main(String)' has value 21.
@@ -140,11 +141,11 @@
javaNcss
5
- 'com.company.money.Foo' has value 68 highest 20.
+ 'com.company.money.Foo' has value 69 highest 21.
'com.company.money.Foo#Foo()' has value 2.
'com.company.money.Foo#Foo(int)' has value 13.
'com.company.money.Foo#foo()' has value 14.
- 'com.company.money.Foo#main(String)' has value 20.
+ 'com.company.money.Foo#main(String)' has value 21.
diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/comments/xml/CommentDefaultAccessModifier.xml b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/comments/xml/CommentDefaultAccessModifier.xml
index 4b8474bef..ee56ce392 100755
--- a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/comments/xml/CommentDefaultAccessModifier.xml
+++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/comments/xml/CommentDefaultAccessModifier.xml
@@ -155,6 +155,28 @@ public enum TestEnum {
+
+
+
+ #536 Constructor with default access modifier should trigger
+ 1
+
+
+
+
+ #536 Enum constructor with implicit private modifier should not trigger
+ 0
+
diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/unusedcode/xml/UnusedPrivateMethod.xml b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/unusedcode/xml/UnusedPrivateMethod.xml
index 1b6642249..b5d810c04 100644
--- a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/unusedcode/xml/UnusedPrivateMethod.xml
+++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/unusedcode/xml/UnusedPrivateMethod.xml
@@ -1531,4 +1531,22 @@ public class Something {
}
]]>
+
+
+ #521 UnusedPrivateMethod returns false positives with primitive data type in map argument
+ 0
+ map = new LinkedHashMap<>();
+ addToMap(map);
+ }
+
+ private void addToMap(Map map) {
+ map.put("foo", new double[]{0., 1.});
+ }
+ }
+ ]]>
+
+
diff --git a/pom.xml b/pom.xml
index 9f3c42896..9c6b65098 100644
--- a/pom.xml
+++ b/pom.xml
@@ -471,7 +471,7 @@ Additionally it includes CPD, the copy-paste-detector. CPD finds duplicated code
nexus-staging-maven-plugin
1.6.8
-
org.eclipse.m2e
@@ -945,6 +945,7 @@ Additionally it includes CPD, the copy-paste-detector. CPD finds duplicated code
pmd-apex
pmd-java8
pmd-ui
+ pmd-doc
diff --git a/src/site/markdown/overview/changelog.md b/src/site/markdown/overview/changelog.md
index 26d98f53d..5e45ae798 100644
--- a/src/site/markdown/overview/changelog.md
+++ b/src/site/markdown/overview/changelog.md
@@ -13,6 +13,8 @@ This is a major release.
* [Java Type Resolution](#Java_Type_Resolution)
* [Metrics Framework](#Metrics_Framework)
* [Configuration Error Reporting](#Configuration_Error_Reporting)
+ * [Java Symbol Table](#Java_Symbol_Table)
+ * [Apex Parser Update](#Apex_Parser_Update)
* [Modified Rules](#Modified_Rules)
* [Removed Rules](#Removed_Rules)
* [Fixed Issues](#Fixed_Issues)
@@ -79,10 +81,32 @@ and include them to such reports.
* The deprecated rule `UseSingleton` has been removed from the ruleset `java-design`. The rule has been renamed
long time ago to `UseUtilityClass`.
+
+#### Java Symbol Table
+
+A [bug in symbol table](https://github.com/pmd/pmd/pull/549/commits/0958621ca884a8002012fc7738308c8dfc24b97c) prevented
+the symbol table analysis to properly match primitive arrays types. The issue [affected the `java-unsedcode/UnusedPrivateMethod`](https://github.com/pmd/pmd/issues/521)
+rule, but other rules may now produce improved results as consequence of this fix.
+
+#### Apex Parser Update
+
+The Apex parser version was bumped, from `1.0-sfdc-187` to `1.0-sfdc-224`. This update let us take full advatange
+of the latest improvements from Salesforce, but introduces some breaking changes:
+* `BlockStatements` are now created for all control structures, even if no brace is used. We have therefore added
+ a `hasCurlyBrace` method to differentiate between both scenarios.
+* New AST node types are available. In particular `CastExpression`, `ConstructorPreamble`, `IllegalStoreExpression`,
+ `MethodBlockStatement`, `Modifier`, `MultiStatement`, `NestedExpression`, `NestedStoreExpression`,
+ `NewKeyValueObjectExpression` and `StatementExecuted`
+* Some nodes have been removed. Such is the case of `TestNode`, `DottedExpression` and `NewNameValueObjectExpression`
+ (replaced by `NewKeyValueObjectExpression`)
+
+Al existing rules have been updated to reflect these changes. If you have custom rules, be sure to update them.
+
### Fixed Issues
* apex
* [#488](https://github.com/pmd/pmd/pull/488): \[apex] Use Apex lexer for CPD
+ * [#489](https://github.com/pmd/pmd/pull/489): \[apex] Update Apex compiler
* [#500](https://github.com/pmd/pmd/issues/500): \[apex] Running through CLI shows jorje optimization messages
* cpp
* [#448](https://github.com/pmd/pmd/issues/448): \[cpp] Write custom CharStream to handle continuation characters
@@ -91,10 +115,14 @@ and include them to such reports.
* [#487](https://github.com/pmd/pmd/pull/487): \[java] Fix typeresolution for anonymous extending object
* [#496](https://github.com/pmd/pmd/issues/496): \[java] processing error on generics inherited from enclosing class
* [#527](https://github.com/pmd/pmd/issues/527): \[java] Lombok getter annotation on enum is not recognized correctly
+* java-comments
+ * [#536](https://github.com/pmd/pmd/issues/536): \[java] CommentDefaultAccessModifierRule ignores constructors
* java-controversial
* [#408](https://github.com/pmd/pmd/issues/408): \[java] DFA not analyzing asserts
* java-sunsecure
* [#468](https://github.com/pmd/pmd/issues/468): \[java] ArrayIsStoredDirectly false positive
+* java-unusedcode
+ * [#521](https://github.com/pmd/pmd/issues/521): \[java] UnusedPrivateMethod returns false positives with primitive data type in map argument
* java-unnecessarycode
* [#412](https://github.com/pmd/pmd/issues/412): \[java] java-unnecessarycode/UnnecessaryFinalModifier missing cases
@@ -129,8 +157,9 @@ and include them to such reports.
* [#524](https://github.com/pmd/pmd/pull/524): \[java] Add support for explicit type arguments with method invocation - [Bendegúz Nagy](https://github.com/WinterGrascph)
* [#525](https://github.com/pmd/pmd/pull/525): \[core] Fix line ending and not ignored files issues - [Matias Comercio](https://github.com/MatiasComercio)
* [#528](https://github.com/pmd/pmd/pull/528): \[core] Fix typo - [Ayoub Kaanich](https://github.com/kayoub5)
+* [#529](https://github.com/pmd/pmd/pull/529): \[java] Abstracted the Java metrics framework - [Clément Fournier](https://github.com/oowekyala)
* [#530](https://github.com/pmd/pmd/pull/530): \[java] Fix issue #527: Lombok getter annotation on enum is not recognized correctly - [Clément Fournier](https://github.com/oowekyala)
* [#535](https://github.com/pmd/pmd/pull/535): \[apex] Fix broken Apex visitor adapter - [Clément Fournier](https://github.com/oowekyala)
-* [#529](https://github.com/pmd/pmd/pull/529): \[java] Abstracted the Java metrics framework - [Clément Fournier](https://github.com/oowekyala)
* [#542](https://github.com/pmd/pmd/pull/542): \[java] Metrics abstraction - [Clément Fournier](https://github.com/oowekyala)
+* [#548](https://github.com/pmd/pmd/pull/548): \[java] Metrics documentation - [Clément Fournier](https://github.com/oowekyala)