diff --git a/docs/pages/pmd/userdocs/cli_reference.md b/docs/pages/pmd/userdocs/cli_reference.md index d4a0d0552a..ca45b101de 100644 --- a/docs/pages/pmd/userdocs/cli_reference.md +++ b/docs/pages/pmd/userdocs/cli_reference.md @@ -182,7 +182,8 @@ Example: * [apex](pmd_rules_apex.html) (Salesforce Apex) * [java](pmd_rules_java.html) - * Supported Versions: 1.3, 1.4, 1.5, 5, 1.6, 6, 1.7, 7, 1.8, 8, 9, 1.9, 10, 1.10, 11, 12, 13 (default), 13-preview + * Supported Versions: 1.3, 1.4, 1.5, 5, 1.6, 6, 1.7, 7, 1.8, 8, 9, 1.9, 10, 1.10, 11, 12, + 13, 13-preview, 14 (default), 14-preview * [ecmascript](pmd_rules_ecmascript.html) (JavaScript) * [jsp](pmd_rules_jsp.html) * [modelica](pmd_rules_modelica.html) diff --git a/docs/pages/pmd/userdocs/tools/ant.md b/docs/pages/pmd/userdocs/tools/ant.md index d1e6c1917d..55a45fc12c 100644 --- a/docs/pages/pmd/userdocs/tools/ant.md +++ b/docs/pages/pmd/userdocs/tools/ant.md @@ -222,8 +222,10 @@ nested element. Possible values are: - + + + diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md index 568f04d7af..32ce1c4c9f 100644 --- a/docs/pages/release_notes.md +++ b/docs/pages/release_notes.md @@ -19,27 +19,79 @@ This is a {{ site.pmd.release_type }} release. ### New and noteworthy +#### Java 14 Support + +This release of PMD brings support for Java 14. PMD can parse [Switch Expressions](https://openjdk.java.net/jeps/361), +which have been promoted to be a standard language feature of Java. + +PMD also parses [Text Blocks](https://openjdk.java.net/jeps/368) as String literals, which is still a preview +language feature in Java 14. + +The new [Pattern Matching for instanceof](https://openjdk.java.net/jeps/305) can be used as well as +[Records](https://openjdk.java.net/jeps/359). + +Note: The Text Blocks, Pattern Matching for instanceof and Records are all preview language features of OpenJDK 14 +and are not enabled by default. In order to +analyze a project with PMD that uses these language features, you'll need to enable it via the environment +variable `PMD_JAVA_OPTS` and select the new language version `14-preview`: + + export PMD_JAVA_OPTS=--enable-preview + ./run.sh pmd -language java -version 14-preview ... + +Note: Support for the extended break statement introduced in Java 12 as a preview language feature +has been removed from PMD with this version. The version "12-preview" is no longer available. + + #### Updated PMD Designer This PMD release ships a new version of the pmd-designer. For the changes, see [PMD Designer Changelog](https://github.com/pmd/pmd-designer/releases/tag/6.21.0). -### Apex Suppressions +#### Apex Suppressions In addition to suppressing violation with the `@SuppressWarnings` annotation, Apex now also supports the suppressions with a `NOPMD` comment. See [Suppressing warnings](pmd_userdocs_suppressing_warnings.html). +#### Improved CPD support for C# + +The C# tokenizer is now based on an antlr grammar instead of a manual written tokenizer. This +should give more accurate results and especially fixes the problems with the using statement syntax +(see [#2139](https://github.com/pmd/pmd/issues/2139)). + +#### New Rules + +* The Rule {% rule "apex/design/CognitiveComplexity" %} (`apex-design`) finds methods and classes + that are highly complex and therefore difficult to read and more costly to maintain. In contrast + to cyclomatic complexity, this rule uses "Cognitive Complexity", which is a measure of how + difficult it is for humans to read and understand a method. + +* The Rule {% rule "apex/errorprone/TestMethodsMustBeInTestClasses" %} (`apex-errorprone`) finds test methods + that are not residing in a test class. The test methods should be moved to a proper test class. + Support for tests inside functional classes was removed in Spring-13 (API Version 27.0), making classes + that violate this rule fail compile-time. This rule is however useful when dealing with legacy code. + ### Fixed Issues * apex * [#1087](https://github.com/pmd/pmd/issues/1087): \[apex] Support suppression via //NOPMD + * [#2306](https://github.com/pmd/pmd/issues/2306): \[apex] Switch statements are not parsed/supported +* apex-design + * [#2162](https://github.com/pmd/pmd/issues/2162): \[apex] Cognitive Complexity rule +* apex-errorprone + * [#639](https://github.com/pmd/pmd/issues/639): \[apex] Test methods should not be in classes other than test classes +* cs + * [#2139](https://github.com/pmd/pmd/issues/2139): \[cs] CPD doesn't understand alternate using statement syntax with C# 8.0 * doc * [#2274](https://github.com/pmd/pmd/issues/2274): \[doc] Java API documentation for PMD * java + * [#2159](https://github.com/pmd/pmd/issues/2159): \[java] Prepare for JDK 14 * [#2268](https://github.com/pmd/pmd/issues/2268): \[java] Improve TypeHelper resilience * java-bestpractices * [#2277](https://github.com/pmd/pmd/issues/2277): \[java] FP in UnusedImports for ambiguous static on-demand imports +* java-design + * [#911](https://github.com/pmd/pmd/issues/911): \[java] UselessOverridingMethod false positive when elevating access modifier * java-errorprone + * [#2242](https://github.com/pmd/pmd/issues/2242): \[java] False-positive MisplacedNullCheck reported * [#2250](https://github.com/pmd/pmd/issues/2250): \[java] InvalidLogMessageFormat flags logging calls using a slf4j-Marker * [#2255](https://github.com/pmd/pmd/issues/2255): \[java] InvalidLogMessageFormat false-positive for a lambda argument * java-performance @@ -119,7 +171,7 @@ methods on {% jdoc apex::lang.apex.ast.ApexParserVisitor %} and its implementati * {% jdoc !!java::lang.java.ast.ASTFormalParameters#getParameterCount() %}. Use {% jdoc java::lang.java.ast.ASTFormalParameters#size() %} instead. * pmd-apex - * {% jdoc java::lang.apex.metrics.ApexMetrics %}, {% jdoc java::lang.java.metrics.JavaMetricsComputer %} + * {% jdoc apex::lang.apex.metrics.ApexMetrics %}, {% jdoc apex::lang.apex.metrics.ApexMetricsComputer %} ### External Contributions @@ -130,6 +182,10 @@ methods on {% jdoc apex::lang.apex.ast.ApexParserVisitor %} and its implementati * [#2276](https://github.com/pmd/pmd/pull/2276): \[java] AppendCharacterWithCharRule ignore literals in expressions - [Kris Scheibe](https://github.com/kris-scheibe) * [#2278](https://github.com/pmd/pmd/pull/2278): \[java] fix UnusedImports rule for ambiguous static on-demand imports - [Kris Scheibe](https://github.com/kris-scheibe) * [#2279](https://github.com/pmd/pmd/pull/2279): \[apex] Add support for suppressing violations using the // NOPMD comment - [Gwilym Kuiper](https://github.com/gwilymatgearset) +* [#2280](https://github.com/pmd/pmd/pull/2280): \[cs] CPD: Replace C# tokenizer by an Antlr-based one - [Maikel Steneker](https://github.com/maikelsteneker) +* [#2297](https://github.com/pmd/pmd/pull/2297): \[apex] Cognitive complexity metrics - [Gwilym Kuiper](https://github.com/gwilymatgearset) +* [#2317](https://github.com/pmd/pmd/pull/2317): \[apex] New Rule - Test Methods Must Be In Test Classes - [Brian Nørremark](https://github.com/noerremark) +* [#2321](https://github.com/pmd/pmd/pull/2321): \[apex] Support switch statements correctly in Cognitive Complexity - [Gwilym Kuiper](https://github.com/gwilymatgearset) {% endtocmaker %} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTElseWhenBlock.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTElseWhenBlock.java new file mode 100644 index 0000000000..4280a35355 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTElseWhenBlock.java @@ -0,0 +1,21 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.ast; + +import apex.jorje.semantic.ast.statement.ElseWhenBlock; + +public final class ASTElseWhenBlock extends AbstractApexNode { + + + ASTElseWhenBlock(ElseWhenBlock node) { + super(node); + } + + + @Override + public Object jjtAccept(ApexParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTEmptyReferenceExpression.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTEmptyReferenceExpression.java new file mode 100644 index 0000000000..39ca6a6dd7 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTEmptyReferenceExpression.java @@ -0,0 +1,22 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.ast; + +import apex.jorje.semantic.ast.expression.EmptyReferenceExpression; + +public final class ASTEmptyReferenceExpression extends AbstractApexNode { + + + ASTEmptyReferenceExpression(EmptyReferenceExpression node) { + super(node); + } + + + @Override + public Object jjtAccept(ApexParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } + +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTIdentifierCase.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTIdentifierCase.java new file mode 100644 index 0000000000..dfd45cf184 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTIdentifierCase.java @@ -0,0 +1,21 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.ast; + +import apex.jorje.semantic.ast.statement.WhenCases.IdentifierCase; + +public final class ASTIdentifierCase extends AbstractApexNode { + + + ASTIdentifierCase(IdentifierCase node) { + super(node); + } + + + @Override + public Object jjtAccept(ApexParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTLiteralCase.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTLiteralCase.java new file mode 100644 index 0000000000..fc97e4bf9f --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTLiteralCase.java @@ -0,0 +1,21 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.ast; + +import apex.jorje.semantic.ast.statement.WhenCases.LiteralCase; + +public final class ASTLiteralCase extends AbstractApexNode { + + + ASTLiteralCase(LiteralCase node) { + super(node); + } + + + @Override + public Object jjtAccept(ApexParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTSwitchStatement.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTSwitchStatement.java new file mode 100644 index 0000000000..10cc291e8c --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTSwitchStatement.java @@ -0,0 +1,21 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.ast; + +import apex.jorje.semantic.ast.statement.SwitchStatement; + +public final class ASTSwitchStatement extends AbstractApexNode { + + + ASTSwitchStatement(SwitchStatement node) { + super(node); + } + + + @Override + public Object jjtAccept(ApexParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTTypeWhenBlock.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTTypeWhenBlock.java new file mode 100644 index 0000000000..f568b9a012 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTTypeWhenBlock.java @@ -0,0 +1,37 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.ast; + +import java.lang.reflect.Field; + +import apex.jorje.semantic.ast.statement.TypeWhenBlock; + +public final class ASTTypeWhenBlock extends AbstractApexNode { + + + ASTTypeWhenBlock(TypeWhenBlock node) { + super(node); + } + + public String getType() { + return String.valueOf(node.getTypeRef()); + } + + public String getName() { + // unfortunately the name is not exposed... + try { + Field nameField = TypeWhenBlock.class.getDeclaredField("name"); + nameField.setAccessible(true); + return String.valueOf(nameField.get(node)); + } catch (SecurityException | ReflectiveOperationException e) { + return null; + } + } + + @Override + public Object jjtAccept(ApexParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTValueWhenBlock.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTValueWhenBlock.java new file mode 100644 index 0000000000..db976e06c4 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTValueWhenBlock.java @@ -0,0 +1,21 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.ast; + +import apex.jorje.semantic.ast.statement.ValueWhenBlock; + +public final class ASTValueWhenBlock extends AbstractApexNode { + + + ASTValueWhenBlock(ValueWhenBlock node) { + super(node); + } + + + @Override + public Object jjtAccept(ApexParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexParserVisitor.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexParserVisitor.java index 2edcc668b7..cb6497ce7d 100644 --- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexParserVisitor.java +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexParserVisitor.java @@ -191,4 +191,18 @@ public interface ApexParserVisitor { Object visit(ASTVariableExpression node, Object data); Object visit(ASTWhileLoopStatement node, Object data); + + Object visit(ASTSwitchStatement node, Object data); + + Object visit(ASTElseWhenBlock node, Object data); + + Object visit(ASTTypeWhenBlock node, Object data); + + Object visit(ASTValueWhenBlock node, Object data); + + Object visit(ASTLiteralCase node, Object data); + + Object visit(ASTIdentifierCase node, Object data); + + Object visit(ASTEmptyReferenceExpression node, Object data); } diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexParserVisitorAdapter.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexParserVisitorAdapter.java index eb58942e54..3a58a233f1 100644 --- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexParserVisitorAdapter.java +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexParserVisitorAdapter.java @@ -468,4 +468,39 @@ public class ApexParserVisitorAdapter implements ApexParserVisitor { public Object visit(ASTFormalComment node, Object data) { return visit((ApexNode) node, data); } + + @Override + public Object visit(ASTSwitchStatement node, Object data) { + return visit((ApexNode) node, data); + } + + @Override + public Object visit(ASTElseWhenBlock node, Object data) { + return visit((ApexNode) node, data); + } + + @Override + public Object visit(ASTTypeWhenBlock node, Object data) { + return visit((ApexNode) node, data); + } + + @Override + public Object visit(ASTValueWhenBlock node, Object data) { + return visit((ApexNode) node, data); + } + + @Override + public Object visit(ASTIdentifierCase node, Object data) { + return visit((ApexNode) node, data); + } + + @Override + public Object visit(ASTLiteralCase node, Object data) { + return visit((ApexNode) node, data); + } + + @Override + public Object visit(ASTEmptyReferenceExpression node, Object data) { + return visit((ApexNode) node, data); + } } diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexTreeBuilder.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexTreeBuilder.java index 53ba9d6f7a..40e1577601 100644 --- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexTreeBuilder.java +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexTreeBuilder.java @@ -40,6 +40,7 @@ import apex.jorje.semantic.ast.expression.BindExpressions; import apex.jorje.semantic.ast.expression.BooleanExpression; import apex.jorje.semantic.ast.expression.CastExpression; import apex.jorje.semantic.ast.expression.ClassRefExpression; +import apex.jorje.semantic.ast.expression.EmptyReferenceExpression; import apex.jorje.semantic.ast.expression.Expression; import apex.jorje.semantic.ast.expression.IllegalStoreExpression; import apex.jorje.semantic.ast.expression.InstanceOfExpression; @@ -93,6 +94,7 @@ import apex.jorje.semantic.ast.statement.DmlUndeleteStatement; import apex.jorje.semantic.ast.statement.DmlUpdateStatement; import apex.jorje.semantic.ast.statement.DmlUpsertStatement; import apex.jorje.semantic.ast.statement.DoLoopStatement; +import apex.jorje.semantic.ast.statement.ElseWhenBlock; import apex.jorje.semantic.ast.statement.ExpressionStatement; import apex.jorje.semantic.ast.statement.FieldDeclaration; import apex.jorje.semantic.ast.statement.FieldDeclarationStatements; @@ -106,10 +108,15 @@ import apex.jorje.semantic.ast.statement.ReturnStatement; import apex.jorje.semantic.ast.statement.RunAsBlockStatement; import apex.jorje.semantic.ast.statement.Statement; import apex.jorje.semantic.ast.statement.StatementExecuted; +import apex.jorje.semantic.ast.statement.SwitchStatement; import apex.jorje.semantic.ast.statement.ThrowStatement; import apex.jorje.semantic.ast.statement.TryCatchFinallyBlockStatement; +import apex.jorje.semantic.ast.statement.TypeWhenBlock; +import apex.jorje.semantic.ast.statement.ValueWhenBlock; import apex.jorje.semantic.ast.statement.VariableDeclaration; import apex.jorje.semantic.ast.statement.VariableDeclarationStatements; +import apex.jorje.semantic.ast.statement.WhenCases.IdentifierCase; +import apex.jorje.semantic.ast.statement.WhenCases.LiteralCase; import apex.jorje.semantic.ast.statement.WhileLoopStatement; import apex.jorje.semantic.ast.visitor.AdditionalPassScope; import apex.jorje.semantic.ast.visitor.AstVisitor; @@ -117,7 +124,8 @@ import apex.jorje.semantic.exception.Errors; public final class ApexTreeBuilder extends AstVisitor { - private static final Map, Constructor>> NODE_TYPE_TO_NODE_ADAPTER_TYPE = new HashMap<>(); + private static final Map, Constructor>> + NODE_TYPE_TO_NODE_ADAPTER_TYPE = new HashMap<>(); static { register(Annotation.class, ASTAnnotation.class); @@ -208,11 +216,19 @@ public final class ApexTreeBuilder extends AstVisitor { register(VariableDeclarationStatements.class, ASTVariableDeclarationStatements.class); register(VariableExpression.class, ASTVariableExpression.class); register(WhileLoopStatement.class, ASTWhileLoopStatement.class); + register(SwitchStatement.class, ASTSwitchStatement.class); + register(ElseWhenBlock.class, ASTElseWhenBlock.class); + register(TypeWhenBlock.class, ASTTypeWhenBlock.class); + register(ValueWhenBlock.class, ASTValueWhenBlock.class); + register(LiteralCase.class, ASTLiteralCase.class); + register(IdentifierCase.class, ASTIdentifierCase.class); + register(EmptyReferenceExpression.class, ASTEmptyReferenceExpression.class); } - private static void register(Class nodeType, Class> nodeAdapterType) { + private static void register(Class nodeType, + Class> nodeAdapterType) { try { - NODE_TYPE_TO_NODE_ADAPTER_TYPE.put(nodeType, nodeAdapterType.getConstructor(nodeType)); + NODE_TYPE_TO_NODE_ADAPTER_TYPE.put(nodeType, nodeAdapterType.getDeclaredConstructor(nodeType)); } catch (SecurityException | NoSuchMethodException e) { throw new RuntimeException(e); } @@ -805,4 +821,39 @@ public final class ApexTreeBuilder extends AstVisitor { public boolean visit(NewKeyValueObjectExpression node, AdditionalPassScope scope) { return visit(node); } + + @Override + public boolean visit(SwitchStatement node, AdditionalPassScope scope) { + return visit(node); + } + + @Override + public boolean visit(ElseWhenBlock node, AdditionalPassScope scope) { + return visit(node); + } + + @Override + public boolean visit(TypeWhenBlock node, AdditionalPassScope scope) { + return visit(node); + } + + @Override + public boolean visit(ValueWhenBlock node, AdditionalPassScope scope) { + return visit(node); + } + + @Override + public boolean visit(LiteralCase node, AdditionalPassScope scope) { + return visit(node); + } + + @Override + public boolean visit(IdentifierCase node, AdditionalPassScope scope) { + return visit(node); + } + + @Override + public boolean visit(EmptyReferenceExpression node, AdditionalPassScope scope) { + return visit(node); + } } diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexClassMetricKey.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexClassMetricKey.java index 2ffae23a42..73a08d5f2c 100644 --- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexClassMetricKey.java +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexClassMetricKey.java @@ -5,6 +5,7 @@ package net.sourceforge.pmd.lang.apex.metrics.api; import net.sourceforge.pmd.lang.apex.ast.ASTUserClassOrInterface; +import net.sourceforge.pmd.lang.apex.metrics.impl.ClassCognitiveComplexityMetric; import net.sourceforge.pmd.lang.apex.metrics.impl.WmcMetric; import net.sourceforge.pmd.lang.metrics.MetricKey; @@ -12,6 +13,7 @@ import net.sourceforge.pmd.lang.metrics.MetricKey; * @author Clément Fournier */ public enum ApexClassMetricKey implements MetricKey> { + COGNITIVE(new ClassCognitiveComplexityMetric()), WMC(new WmcMetric()); diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexOperationMetricKey.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexOperationMetricKey.java index 502dbbd069..e5816c5e6c 100644 --- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexOperationMetricKey.java +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexOperationMetricKey.java @@ -5,6 +5,7 @@ package net.sourceforge.pmd.lang.apex.metrics.api; import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.metrics.impl.CognitiveComplexityMetric; import net.sourceforge.pmd.lang.apex.metrics.impl.CycloMetric; import net.sourceforge.pmd.lang.metrics.MetricKey; @@ -12,7 +13,8 @@ import net.sourceforge.pmd.lang.metrics.MetricKey; * @author Clément Fournier */ public enum ApexOperationMetricKey implements MetricKey { - CYCLO(new CycloMetric()); + CYCLO(new CycloMetric()), + COGNITIVE(new CognitiveComplexityMetric()); private final ApexOperationMetric calculator; diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/ClassCognitiveComplexityMetric.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/ClassCognitiveComplexityMetric.java new file mode 100644 index 0000000000..90e0527c14 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/ClassCognitiveComplexityMetric.java @@ -0,0 +1,23 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.impl; + +import net.sourceforge.pmd.lang.apex.ast.ASTUserClassOrInterface; +import net.sourceforge.pmd.lang.apex.metrics.ApexMetrics; +import net.sourceforge.pmd.lang.apex.metrics.api.ApexOperationMetricKey; +import net.sourceforge.pmd.lang.metrics.MetricOptions; +import net.sourceforge.pmd.lang.metrics.ResultOption; + +/** + * The sum of the cognitive complexities of all the methods within a class. + * + * @author Gwilym Kuiper + */ +public class ClassCognitiveComplexityMetric extends AbstractApexClassMetric { + @Override + public double computeFor(ASTUserClassOrInterface node, MetricOptions options) { + return ApexMetrics.get(ApexOperationMetricKey.COGNITIVE, node, ResultOption.SUM); + } +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/CognitiveComplexityMetric.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/CognitiveComplexityMetric.java new file mode 100644 index 0000000000..740bfd29bb --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/CognitiveComplexityMetric.java @@ -0,0 +1,24 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.impl; + +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.metrics.impl.visitors.CognitiveComplexityVisitor; +import net.sourceforge.pmd.lang.metrics.MetricOptions; + +/** + * Measures the cognitive complexity of a Class / Method in Apex. + * + * See https://www.sonarsource.com/docs/CognitiveComplexity.pdf for information about the metric + * + * @author Gwilym Kuiper + */ +public class CognitiveComplexityMetric extends AbstractApexOperationMetric { + @Override + public double computeFor(ASTMethod node, MetricOptions options) { + CognitiveComplexityVisitor.State resultingState = (CognitiveComplexityVisitor.State) node.jjtAccept(new CognitiveComplexityVisitor(), new CognitiveComplexityVisitor.State()); + return resultingState.getComplexity(); + } +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/visitors/CognitiveComplexityVisitor.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/visitors/CognitiveComplexityVisitor.java new file mode 100644 index 0000000000..9cfb32b325 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/visitors/CognitiveComplexityVisitor.java @@ -0,0 +1,246 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.impl.visitors; + +import net.sourceforge.pmd.lang.apex.ast.ASTBlockStatement; +import net.sourceforge.pmd.lang.apex.ast.ASTBooleanExpression; +import net.sourceforge.pmd.lang.apex.ast.ASTBreakStatement; +import net.sourceforge.pmd.lang.apex.ast.ASTCatchBlockStatement; +import net.sourceforge.pmd.lang.apex.ast.ASTContinueStatement; +import net.sourceforge.pmd.lang.apex.ast.ASTDoLoopStatement; +import net.sourceforge.pmd.lang.apex.ast.ASTForEachStatement; +import net.sourceforge.pmd.lang.apex.ast.ASTForLoopStatement; +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.ASTPrefixExpression; +import net.sourceforge.pmd.lang.apex.ast.ASTSwitchStatement; +import net.sourceforge.pmd.lang.apex.ast.ASTTernaryExpression; +import net.sourceforge.pmd.lang.apex.ast.ASTWhileLoopStatement; +import net.sourceforge.pmd.lang.apex.ast.ApexNode; +import net.sourceforge.pmd.lang.apex.ast.ApexParserVisitorAdapter; + +import apex.jorje.data.ast.BooleanOp; +import apex.jorje.data.ast.PrefixOp; + +/** + * @author Gwilym Kuiper + */ +public class CognitiveComplexityVisitor extends ApexParserVisitorAdapter { + public static class State { + private int complexity = 0; + private int nestingLevel = 0; + + private BooleanOp currentBooleanOperation = null; + private String methodName = null; + + public double getComplexity() { + return complexity; + } + + void structureComplexity() { + complexity += 1; + } + + void nestingComplexity() { + complexity += nestingLevel; + } + + void booleanOperation(BooleanOp op) { + if (currentBooleanOperation != op) { + if (op != null) { + structureComplexity(); + } + + currentBooleanOperation = op; + } + } + + void increaseNestingLevel() { + structureComplexity(); + nestingComplexity(); + nestingLevel++; + } + + void decreaseNestingLevel() { + nestingLevel--; + } + + void methodCall(String methodCalledName) { + if (methodCalledName.equals(methodName)) { + structureComplexity(); + } + } + + void setMethodName(String name) { + methodName = name; + } + } + + @Override + public Object visit(ASTIfElseBlockStatement node, Object data) { + State state = (State) data; + + boolean hasElseStatement = node.getNode().hasElseStatement(); + for (ApexNode child : node.children()) { + // If we don't have an else statement, we get an empty block statement which we shouldn't count + if (!hasElseStatement && child instanceof ASTBlockStatement) { + break; + } + + state.increaseNestingLevel(); + super.visit(child, data); + state.decreaseNestingLevel(); + } + + return data; + } + + @Override + public Object visit(ASTForLoopStatement node, Object data) { + State state = (State) data; + + state.increaseNestingLevel(); + super.visit(node, data); + state.decreaseNestingLevel(); + + return data; + } + + @Override + public Object visit(ASTForEachStatement node, Object data) { + State state = (State) data; + + state.increaseNestingLevel(); + super.visit(node, data); + state.decreaseNestingLevel(); + + return data; + } + + @Override + public Object visit(ASTContinueStatement node, Object data) { + State state = (State) data; + + state.structureComplexity(); + return super.visit(node, data); + } + + @Override + public Object visit(ASTBreakStatement node, Object data) { + State state = (State) data; + + state.structureComplexity(); + return super.visit(node, data); + } + + @Override + public Object visit(ASTWhileLoopStatement node, Object data) { + State state = (State) data; + + state.increaseNestingLevel(); + super.visit(node, data); + state.decreaseNestingLevel(); + + return data; + } + + @Override + public Object visit(ASTCatchBlockStatement node, Object data) { + State state = (State) data; + + state.increaseNestingLevel(); + super.visit(node, data); + state.decreaseNestingLevel(); + + return data; + } + + @Override + public Object visit(ASTDoLoopStatement node, Object data) { + State state = (State) data; + + state.increaseNestingLevel(); + super.visit(node, data); + state.decreaseNestingLevel(); + + return data; + } + + @Override + public Object visit(ASTTernaryExpression node, Object data) { + State state = (State) data; + + state.increaseNestingLevel(); + super.visit(node, data); + state.decreaseNestingLevel(); + + return data; + } + + @Override + public Object visit(ASTBooleanExpression node, Object data) { + State state = (State) data; + + BooleanOp op = node.getNode().getOp(); + if (op == BooleanOp.AND || op == BooleanOp.OR) { + state.booleanOperation(op); + } + + return super.visit(node, data); + } + + @Override + public Object visit(ASTPrefixExpression node, Object data) { + State state = (State) data; + + PrefixOp op = node.getNode().getOp(); + if (op == PrefixOp.NOT) { + state.booleanOperation(null); + } + + return super.visit(node, data); + } + + @Override + public Object visit(ASTBlockStatement node, Object data) { + State state = (State) data; + + for (ApexNode child : node.children()) { + child.jjtAccept(this, data); + + // This needs to happen because the current 'run' of boolean operations is terminated + // once we finish a statement. + state.booleanOperation(null); + } + + return data; + } + + @Override + public Object visit(ASTMethod node, Object data) { + State state = (State) data; + state.setMethodName(node.getNode().getMethodInfo().getCanonicalName()); + return super.visit(node, data); + } + + @Override + public Object visit(ASTMethodCallExpression node, Object data) { + State state = (State) data; + state.methodCall(node.getNode().getMethodName()); + return super.visit(node, data); + } + + @Override + public Object visit(ASTSwitchStatement node, Object data) { + State state = (State) data; + + state.increaseNestingLevel(); + super.visit(node, data); + state.decreaseNestingLevel(); + + return state; + } +} 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 253e9277af..2ec1c22f1f 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 @@ -36,6 +36,8 @@ 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.ASTDoLoopStatement; +import net.sourceforge.pmd.lang.apex.ast.ASTElseWhenBlock; +import net.sourceforge.pmd.lang.apex.ast.ASTEmptyReferenceExpression; import net.sourceforge.pmd.lang.apex.ast.ASTExpression; import net.sourceforge.pmd.lang.apex.ast.ASTExpressionStatement; import net.sourceforge.pmd.lang.apex.ast.ASTField; @@ -44,12 +46,14 @@ import net.sourceforge.pmd.lang.apex.ast.ASTFieldDeclarationStatements; import net.sourceforge.pmd.lang.apex.ast.ASTForEachStatement; import net.sourceforge.pmd.lang.apex.ast.ASTForLoopStatement; import net.sourceforge.pmd.lang.apex.ast.ASTFormalComment; +import net.sourceforge.pmd.lang.apex.ast.ASTIdentifierCase; 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.ASTLiteralCase; import net.sourceforge.pmd.lang.apex.ast.ASTLiteralExpression; import net.sourceforge.pmd.lang.apex.ast.ASTMapEntryNode; import net.sourceforge.pmd.lang.apex.ast.ASTMethod; @@ -84,18 +88,21 @@ 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.ASTSwitchStatement; import net.sourceforge.pmd.lang.apex.ast.ASTTernaryExpression; import net.sourceforge.pmd.lang.apex.ast.ASTThisMethodCallExpression; import net.sourceforge.pmd.lang.apex.ast.ASTThisVariableExpression; import net.sourceforge.pmd.lang.apex.ast.ASTThrowStatement; import net.sourceforge.pmd.lang.apex.ast.ASTTriggerVariableExpression; import net.sourceforge.pmd.lang.apex.ast.ASTTryCatchFinallyBlockStatement; +import net.sourceforge.pmd.lang.apex.ast.ASTTypeWhenBlock; import net.sourceforge.pmd.lang.apex.ast.ASTUserClass; import net.sourceforge.pmd.lang.apex.ast.ASTUserClassMethods; import net.sourceforge.pmd.lang.apex.ast.ASTUserEnum; import net.sourceforge.pmd.lang.apex.ast.ASTUserExceptionMethods; import net.sourceforge.pmd.lang.apex.ast.ASTUserInterface; import net.sourceforge.pmd.lang.apex.ast.ASTUserTrigger; +import net.sourceforge.pmd.lang.apex.ast.ASTValueWhenBlock; import net.sourceforge.pmd.lang.apex.ast.ASTVariableDeclaration; import net.sourceforge.pmd.lang.apex.ast.ASTVariableDeclarationStatements; import net.sourceforge.pmd.lang.apex.ast.ASTVariableExpression; @@ -600,4 +607,39 @@ public abstract class AbstractApexRule extends AbstractRule public Object visit(ASTFormalComment node, Object data) { return visit((ApexNode) node, data); } + + @Override + public Object visit(ASTSwitchStatement node, Object data) { + return visit((ApexNode) node, data); + } + + @Override + public Object visit(ASTElseWhenBlock node, Object data) { + return visit((ApexNode) node, data); + } + + @Override + public Object visit(ASTTypeWhenBlock node, Object data) { + return visit((ApexNode) node, data); + } + + @Override + public Object visit(ASTValueWhenBlock node, Object data) { + return visit((ApexNode) node, data); + } + + @Override + public Object visit(ASTIdentifierCase node, Object data) { + return visit((ApexNode) node, data); + } + + @Override + public Object visit(ASTLiteralCase node, Object data) { + return visit((ApexNode) node, data); + } + + @Override + public Object visit(ASTEmptyReferenceExpression node, Object data) { + return visit((ApexNode) node, data); + } } diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/design/CognitiveComplexityRule.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/design/CognitiveComplexityRule.java new file mode 100644 index 0000000000..319afdd529 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/design/CognitiveComplexityRule.java @@ -0,0 +1,107 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.rule.design; + +import static net.sourceforge.pmd.properties.constraints.NumericConstraints.positive; + +import java.util.Stack; + +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.ast.ASTUserClass; +import net.sourceforge.pmd.lang.apex.ast.ASTUserTrigger; +import net.sourceforge.pmd.lang.apex.metrics.ApexMetrics; +import net.sourceforge.pmd.lang.apex.metrics.api.ApexClassMetricKey; +import net.sourceforge.pmd.lang.apex.metrics.api.ApexOperationMetricKey; +import net.sourceforge.pmd.lang.apex.rule.AbstractApexRule; +import net.sourceforge.pmd.lang.metrics.MetricsUtil; +import net.sourceforge.pmd.lang.metrics.ResultOption; +import net.sourceforge.pmd.properties.PropertyDescriptor; +import net.sourceforge.pmd.properties.PropertyFactory; + +public class CognitiveComplexityRule extends AbstractApexRule { + + private static final PropertyDescriptor CLASS_LEVEL_DESCRIPTOR + = PropertyFactory.intProperty("classReportLevel") + .desc("Total class cognitive complexity reporting threshold") + .require(positive()) + .defaultValue(50) + .build(); + + private static final PropertyDescriptor METHOD_LEVEL_DESCRIPTOR + = PropertyFactory.intProperty("methodReportLevel") + .desc("Cognitive complexity reporting threshold") + .require(positive()) + .defaultValue(15) + .build(); + + private Stack classNames = new Stack<>(); + private boolean inTrigger; + + + public CognitiveComplexityRule() { + definePropertyDescriptor(CLASS_LEVEL_DESCRIPTOR); + definePropertyDescriptor(METHOD_LEVEL_DESCRIPTOR); + } + + + @Override + public Object visit(ASTUserTrigger node, Object data) { + inTrigger = true; + super.visit(node, data); + inTrigger = false; + return data; + } + + + @Override + public Object visit(ASTUserClass node, Object data) { + + classNames.push(node.getImage()); + super.visit(node, data); + classNames.pop(); + + if (ApexClassMetricKey.COGNITIVE.supports(node)) { + int classCognitive = (int) MetricsUtil.computeMetric(ApexClassMetricKey.COGNITIVE, node); + + if (classCognitive >= getProperty(CLASS_LEVEL_DESCRIPTOR)) { + int classHighest = (int) ApexMetrics.get(ApexOperationMetricKey.COGNITIVE, node, ResultOption.HIGHEST); + + String[] messageParams = { + "class", + node.getImage(), + " total", + classCognitive + " (highest " + classHighest + ")", + }; + + addViolation(data, node, messageParams); + } + } + return data; + } + + + @Override + public final Object visit(ASTMethod node, Object data) { + + if (ApexOperationMetricKey.COGNITIVE.supports(node)) { + int cognitive = (int) MetricsUtil.computeMetric(ApexOperationMetricKey.COGNITIVE, node); + if (cognitive >= getProperty(METHOD_LEVEL_DESCRIPTOR)) { + String opType = inTrigger ? "trigger" + : node.getImage().equals(classNames.peek()) ? "constructor" + : "method"; + + addViolation(data, node, new String[] { + opType, + node.getQualifiedName().getOperation(), + "", + "" + cognitive, + }); + } + } + + return data; + } + +} diff --git a/pmd-apex/src/main/resources/category/apex/design.xml b/pmd-apex/src/main/resources/category/apex/design.xml index 1b87d42416..bef21cd3f7 100644 --- a/pmd-apex/src/main/resources/category/apex/design.xml +++ b/pmd-apex/src/main/resources/category/apex/design.xml @@ -86,6 +86,65 @@ public class Complicated { + + +Methods that are highly complex are difficult to read and more costly to maintain. If you include too much decisional +logic within a single method, you make its behavior hard to understand and more difficult to modify. + +Cognitive complexity is a measure of how difficult it is for humans to read and understand a method. Code that contains +a break in the control flow is more complex, whereas the use of language shorthands doesn't increase the level of +complexity. Nested control flows can make a method more difficult to understand, with each additional nesting of the +control flow leading to an increase in cognitive complexity. + +Information about Cognitive complexity can be found in the original paper here: +https://www.sonarsource.com/docs/CognitiveComplexity.pdf + +By default, this rule reports methods with a complexity of 15 or more. Reported methods should be broken down into less +complex components. + + 3 + + contacts) { + List contactsToUpdate = new List(); + + for (Contact contact : contacts) { // +1 + if (contact.Department == 'Finance') { // +2 + contact.Title = 'Finance Specialist'; + contactsToUpdate.add(contact); + } else if (contact.Department == 'Sales') { // +2 + contact.Title = 'Sales Specialist'; + contactsToUpdate.add(contact); + } + } + + update contactsToUpdate; + } +} +]]> + + + + + + + Test methods marked as a testMethod or annotated with @IsTest, + but not residing in a test class should be moved to a proper + class or have the @IsTest annotation added to the class. + + Support for tests inside functional classes was removed in Spring-13 (API Version 27.0), + making classes that violate this rule fail compile-time. This rule is mostly usable when + dealing with legacy code. + + 3 + + + + + + + + + + + + diff --git a/pmd-apex/src/main/resources/rulesets/apex/quickstart.xml b/pmd-apex/src/main/resources/rulesets/apex/quickstart.xml index 7c98768dcb..9d20e8bfcf 100644 --- a/pmd-apex/src/main/resources/rulesets/apex/quickstart.xml +++ b/pmd-apex/src/main/resources/rulesets/apex/quickstart.xml @@ -63,6 +63,7 @@ 3 + @@ -123,6 +124,7 @@ + diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/ast/ASTSwitchStatementTest.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/ast/ASTSwitchStatementTest.java new file mode 100644 index 0000000000..72623c8ecc --- /dev/null +++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/ast/ASTSwitchStatementTest.java @@ -0,0 +1,46 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.ast; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import apex.jorje.semantic.ast.compilation.Compilation; + +public class ASTSwitchStatementTest extends ApexParserTestBase { + + @Test + public void testExamples() { + ApexNode node = parseResource("SwitchStatements.cls"); + List switchStatements = node.findDescendantsOfType(ASTSwitchStatement.class); + Assert.assertEquals(4, switchStatements.size()); + + Assert.assertTrue(switchStatements.get(0).getChild(0) instanceof ASTVariableExpression); + Assert.assertEquals(5, switchStatements.get(0).findChildrenOfType(ASTValueWhenBlock.class).size()); + Assert.assertEquals(3, switchStatements.get(0).findChildrenOfType(ASTValueWhenBlock.class) + .get(1).findChildrenOfType(ASTLiteralCase.class).size()); + Assert.assertEquals(1, switchStatements.get(0).findChildrenOfType(ASTElseWhenBlock.class).size()); + + Assert.assertTrue(switchStatements.get(1).getChild(0) instanceof ASTMethodCallExpression); + Assert.assertEquals(2, switchStatements.get(1).findChildrenOfType(ASTValueWhenBlock.class).size()); + Assert.assertEquals(1, switchStatements.get(1).findChildrenOfType(ASTElseWhenBlock.class).size()); + + Assert.assertTrue(switchStatements.get(2).getChild(0) instanceof ASTVariableExpression); + Assert.assertEquals(2, switchStatements.get(2).findChildrenOfType(ASTTypeWhenBlock.class).size()); + Assert.assertEquals("Account", switchStatements.get(2).findChildrenOfType(ASTTypeWhenBlock.class) + .get(0).getType()); + Assert.assertEquals("a", switchStatements.get(2).findChildrenOfType(ASTTypeWhenBlock.class) + .get(0).getName()); + Assert.assertEquals(1, switchStatements.get(2).findChildrenOfType(ASTValueWhenBlock.class).size()); + Assert.assertEquals(1, switchStatements.get(2).findChildrenOfType(ASTElseWhenBlock.class).size()); + + Assert.assertTrue(switchStatements.get(3).getChild(0) instanceof ASTVariableExpression); + Assert.assertEquals(2, switchStatements.get(3).findChildrenOfType(ASTValueWhenBlock.class).size()); + Assert.assertEquals(1, switchStatements.get(3).findChildrenOfType(ASTElseWhenBlock.class).size()); + } + +} 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 79ecc234c6..0cc7c50297 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 @@ -179,7 +179,7 @@ public class ApexParserTest extends ApexParserTestBase { Assert.assertNotNull(rootNode); int count = visitPosition(rootNode, 0); - Assert.assertEquals(427, count); + Assert.assertEquals(487, count); } private int visitPosition(Node node, int count) { diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/AllMetricsTest.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/AllMetricsTest.java index 4f60d06fa6..f829c0ee65 100644 --- a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/AllMetricsTest.java +++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/AllMetricsTest.java @@ -20,6 +20,7 @@ public class AllMetricsTest extends SimpleAggregatorTst { public void setUp() { addRule(RULESET, "CycloTest"); addRule(RULESET, "WmcTest"); + addRule(RULESET, "CognitiveComplexityTest"); } } diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/CognitiveComplexityTestRule.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/CognitiveComplexityTestRule.java new file mode 100644 index 0000000000..5bb127e5e7 --- /dev/null +++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/CognitiveComplexityTestRule.java @@ -0,0 +1,23 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.impl; + +import net.sourceforge.pmd.lang.apex.metrics.api.ApexClassMetricKey; +import net.sourceforge.pmd.lang.apex.metrics.api.ApexOperationMetricKey; + +/** + * @author Gwilym Kuiper + */ +public class CognitiveComplexityTestRule extends AbstractApexMetricTestRule { + @Override + protected ApexClassMetricKey getClassKey() { + return null; + } + + @Override + protected ApexOperationMetricKey getOpKey() { + return ApexOperationMetricKey.COGNITIVE; + } +} diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/rule/errorprone/TestMethodsMustBeInTestClassesTest.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/rule/errorprone/TestMethodsMustBeInTestClassesTest.java new file mode 100644 index 0000000000..6557797677 --- /dev/null +++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/rule/errorprone/TestMethodsMustBeInTestClassesTest.java @@ -0,0 +1,11 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.rule.errorprone; + +import net.sourceforge.pmd.testframework.PmdRuleTst; + +public class TestMethodsMustBeInTestClassesTest extends PmdRuleTst { + // no additional unit tests +} diff --git a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/ast/SwitchStatements.cls b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/ast/SwitchStatements.cls new file mode 100644 index 0000000000..621ab5c727 --- /dev/null +++ b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/ast/SwitchStatements.cls @@ -0,0 +1,74 @@ +public class ApexSwitchStatements { + public void example1() { + int i = 3; + switch on i { + when 2 { + System.debug('when block 2'); + } + when 3, 4, 5 { + System.debug('when block 3 and 4 and 5'); + } + when 6, 7 { + System.debug('when block 6 and 7'); + } + when -3 { + System.debug('when block -3'); + } + when null { + System.debug('bad integer'); + } + when else { + System.debug('default'); + } + } + } + + public void example2() { + int i = 1; + switch on someInteger(i) { + when 2 { + System.debug('when block 2'); + } + when 3 { + System.debug('when block 3'); + } + when else { + System.debug('default'); + } + } + } + private int someInteger(int i) { + return i + 1; + } + + public void example3() { + switch on sobject { + when Account a { + System.debug('account ' + a); + } + when Contact c { + System.debug('contact ' + c); + } + when null { + System.debug('null'); + } + when else { + System.debug('default'); + } + } + } + + public void example4() { + switch on season { + when WINTER { + System.debug('boots'); + } + when SPRING, SUMMER { + System.debug('sandals'); + } + when else { + System.debug('none of the above'); + } + } + } +} \ No newline at end of file diff --git a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/metrics/impl/xml/CognitiveComplexityTest.xml b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/metrics/impl/xml/CognitiveComplexityTest.xml new file mode 100644 index 0000000000..4ab68beecc --- /dev/null +++ b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/metrics/impl/xml/CognitiveComplexityTest.xml @@ -0,0 +1,393 @@ + + + + If statements have complexity 1 + 1 + + 'c__Foo#foo(Integer)' has value 1. + + + + + + + + Nested if statements bump complexity level + 1 + + 'c__Foo#foo(Integer)' has value 3. + + + 0) { // +1 + if (n == 1) { // +2 + return 'one'; + } + + return 'positive'; + } + + return 'negative or 0'; + } + } + ]]> + + + + + Non nested if statements don't incur a penalty + 1 + + 'c__Foo#foo(Integer)' has value 3. + + + 0) { // +1 + return 'positive'; + } + + if (n == 0) { // +1 + return 'zero'; + } + + if (n < 0) { // +1 + return 'negative'; + } + } + } + ]]> + + + + + Else-if blocks count as non-nested + 1 + + 'c__Foo#foo(Integer)' has value 3. + + + 0) { // +1 + return 'positive'; + } else if (n < 0) { // +1 + return 'negative'; + } else { // +1 + return 'zero'; + } + } + } + ]]> + + + + + For loops increment nesting + 1 + + 'c__Foo#foo()' has value 3. + + + + + + + + Foreach loops increment nesting + 1 + + 'c__Foo#foo()' has value 3. + + + + + + + + Continue statements increase complexity + 1 + + 'c__Foo#foo()' has value 4. + + + + + + + + Break statements increase complexity + 1 + + 'c__Foo#foo()' has value 4. + + + + + + + + While statements increase nesting + 1 + + 'c__Foo#foo()' has value 3. + + + 1000) { // +2 + return 'big'; + } + } + + return 'small'; + } + } + ]]> + + + + + Only the catch statement increases nesting + 1 + + 'c__Foo#foo()' has value 4. + + + + + + + + Do-while loops cause nesting + 1 + + 'c__Foo#foo()' has value 5. + + + + + + + + Ternary operators cause nesting + 1 + + 'c__Foo#foo(Integer)' has value 3. + + + 0 ? // +2 + 1 : 0; + } + } + ]]> + + + + + Boolean operators + 6 + + 'c__Foo#a(Integer)' has value 1. + 'c__Foo#b(Integer)' has value 1. + 'c__Foo#c(Integer)' has value 1. + 'c__Foo#d(Boolean,Boolean,Boolean,Boolean,Boolean,Boolean)' has value 3. + 'c__Foo#e(Boolean,Boolean,Boolean)' has value 2. + 'c__Foo#f()' has value 2. + + + 0 && n > 1; // +1 + } + + Boolean b(Integer n) { + return n > 0 && n > 1 && n > 2; // +1 + } + + Boolean c(Integer n) { + return n > 0 || n < 0; // +1 + } + + Boolean d(Boolean a, Boolean b, Boolean c, Boolean d, Boolean e, Boolean f) { + return (a + && b && c) // +1 + || (d || e) // +1 + && f; // +1 + } + + Boolean e(Boolean a, Boolean b, Boolean c) { + return a + && // +1 + !(b && c); // +1 + } + + Boolean f() { + Boolean a = true && false; // +1 + return a && true; // +1 + } + } + ]]> + + + + + Recursion bumps complexity value + 1 + + 'c__Foo#foo(Integer)' has value 3. + + + + + + + + Switch statements only gain 1 complexity regardless of the number of cases + 1 + + 'c__Foo#foo(Integer)' has value 3. + + + + + + \ No newline at end of file diff --git a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/errorprone/xml/TestMethodsMustBeInTestClasses.xml b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/errorprone/xml/TestMethodsMustBeInTestClasses.xml new file mode 100644 index 0000000000..92b2da3d68 --- /dev/null +++ b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/errorprone/xml/TestMethodsMustBeInTestClasses.xml @@ -0,0 +1,57 @@ + + + + Class without @IsTest, method with @IsTest + 1 + 1 + + + + + Class without @IsTest, method with testMethod + 1 + 1 + + + + + Class with @IsTest, method with @IsTest + 0 + + + + + Class with @IsTest, method with testMethod + 0 + + + + \ No newline at end of file diff --git a/pmd-apex/src/test/resources/rulesets/apex/metrics_test.xml b/pmd-apex/src/test/resources/rulesets/apex/metrics_test.xml index b9980f7571..cbb1c7324f 100644 --- a/pmd-apex/src/test/resources/rulesets/apex/metrics_test.xml +++ b/pmd-apex/src/test/resources/rulesets/apex/metrics_test.xml @@ -19,4 +19,9 @@ class="net.sourceforge.pmd.lang.apex.metrics.impl.WmcTestRule"> + + + diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/token/internal/BaseTokenFilter.java b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/token/internal/BaseTokenFilter.java index 747619b5f3..7b7f11ca17 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/cpd/token/internal/BaseTokenFilter.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cpd/token/internal/BaseTokenFilter.java @@ -4,6 +4,12 @@ package net.sourceforge.pmd.cpd.token.internal; +import static net.sourceforge.pmd.internal.util.IteratorUtil.AbstractIterator; + +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.LinkedList; + import net.sourceforge.pmd.cpd.token.TokenFilter; import net.sourceforge.pmd.lang.TokenManager; import net.sourceforge.pmd.lang.ast.GenericToken; @@ -15,7 +21,10 @@ import net.sourceforge.pmd.lang.ast.GenericToken; public abstract class BaseTokenFilter implements TokenFilter { private final TokenManager tokenManager; + private final LinkedList unprocessedTokens; // NOPMD - used both as Queue and List + private final Iterable remainingTokens; private boolean discardingSuppressing; + private T currentToken; /** * Creates a new BaseTokenFilter @@ -23,13 +32,21 @@ public abstract class BaseTokenFilter implements TokenFi */ public BaseTokenFilter(final TokenManager tokenManager) { this.tokenManager = tokenManager; + this.unprocessedTokens = new LinkedList<>(); + this.remainingTokens = new RemainingTokens(); } @Override public final T getNextToken() { - T currentToken = (T) tokenManager.getNextToken(); + currentToken = null; + if (!unprocessedTokens.isEmpty()) { + currentToken = unprocessedTokens.poll(); + return currentToken; + } + currentToken = (T) tokenManager.getNextToken(); while (!shouldStopProcessing(currentToken)) { analyzeToken(currentToken); + analyzeTokens(currentToken, remainingTokens); processCPDSuppression(currentToken); if (!isDiscarding()) { @@ -73,6 +90,18 @@ public abstract class BaseTokenFilter implements TokenFi // noop } + /** + * Extension point for subclasses to analyze all tokens (before filtering) + * and update internal status to decide on custom discard rules. + * + * @param currentToken The token to be analyzed + * @param remainingTokens All upcoming tokens + * @see #isLanguageSpecificDiscarding() + */ + protected void analyzeTokens(final T currentToken, final Iterable remainingTokens) { + // noop + } + /** * Extension point for subclasses to indicate tokens are to be filtered. * @@ -90,4 +119,43 @@ public abstract class BaseTokenFilter implements TokenFi */ protected abstract boolean shouldStopProcessing(T currentToken); + private class RemainingTokens implements Iterable { + + @Override + public Iterator iterator() { + return new RemainingTokensIterator(currentToken); + } + + private class RemainingTokensIterator extends AbstractIterator implements Iterator { + + int index = 0; // index of next element + T startToken; + + RemainingTokensIterator(final T startToken) { + this.startToken = startToken; + } + + @Override + protected void computeNext() { + assert index >= 0; + if (startToken != currentToken) { // NOPMD - intentional check for reference equality + throw new ConcurrentModificationException("Using iterator after next token has been requested."); + } + if (index < unprocessedTokens.size()) { + setNext(unprocessedTokens.get(index++)); + } else { + final T nextToken = (T) tokenManager.getNextToken(); + if (shouldStopProcessing(nextToken)) { + done(); + return; + } + index++; + unprocessedTokens.add(nextToken); + setNext(nextToken); + } + } + + } + } + } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/IteratorUtil.java b/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/IteratorUtil.java index c177a9fc90..73056354fb 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/IteratorUtil.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/IteratorUtil.java @@ -358,7 +358,7 @@ public final class IteratorUtil { }; } - private abstract static class AbstractIterator implements Iterator { + public abstract static class AbstractIterator implements Iterator { private State state = State.NOT_READY; private T next = null; @@ -405,5 +405,11 @@ public final class IteratorUtil { READY, NOT_READY, DONE } + @Deprecated + @Override + public final void remove() { + throw new UnsupportedOperationException(); + } + } } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/BaseLanguageModule.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/BaseLanguageModule.java index 721ecaa863..a9bde2c945 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/BaseLanguageModule.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/BaseLanguageModule.java @@ -53,6 +53,8 @@ public abstract class BaseLanguageModule implements Language { } if (isDefault) { + assert defaultVersion == null + : "Default version already set to " + defaultVersion + ", cannot set it to " + languageVersion; defaultVersion = languageVersion; } } @@ -112,6 +114,7 @@ public abstract class BaseLanguageModule implements Language { @Override public LanguageVersion getDefaultVersion() { + assert defaultVersion != null : "Null default version for language " + this; return defaultVersion; } diff --git a/pmd-core/src/main/resources/rulesets/releases/6220.xml b/pmd-core/src/main/resources/rulesets/releases/6220.xml new file mode 100644 index 0000000000..13b0cbc5b7 --- /dev/null +++ b/pmd-core/src/main/resources/rulesets/releases/6220.xml @@ -0,0 +1,14 @@ + + + + +This ruleset contains links to rules that are new in PMD v6.22.0 + + + + + + diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/cpd/token/internal/BaseTokenFilterTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/cpd/token/internal/BaseTokenFilterTest.java new file mode 100644 index 0000000000..2478e0c913 --- /dev/null +++ b/pmd-core/src/test/java/net/sourceforge/pmd/cpd/token/internal/BaseTokenFilterTest.java @@ -0,0 +1,190 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.cpd.token.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import org.junit.Test; + +import net.sourceforge.pmd.lang.TokenManager; +import net.sourceforge.pmd.lang.ast.GenericToken; + +public class BaseTokenFilterTest { + + class StringToken implements GenericToken { + + private final String text; + + StringToken(final String text) { + this.text = text; + } + + @Override + public GenericToken getNext() { + return null; + } + + @Override + public GenericToken getPreviousComment() { + return null; + } + + @Override + public String getImage() { + return text; + } + + @Override + public int getBeginLine() { + return 0; + } + + @Override + public int getEndLine() { + return 0; + } + + @Override + public int getBeginColumn() { + return 0; + } + + @Override + public int getEndColumn() { + return 0; + } + } + + class StringTokenManager implements TokenManager { + + Iterator iterator = Collections.unmodifiableList(Arrays.asList("a", "b", "c")).iterator(); + + @Override + public Object getNextToken() { + if (iterator.hasNext()) { + return new StringToken(iterator.next()); + } else { + return null; + } + } + + @Override + public void setFileName(final String fileName) { + } + } + + class DummyTokenFilter extends BaseTokenFilter { + + Iterable remainingTokens; + + DummyTokenFilter(final TokenManager tokenManager) { + super(tokenManager); + } + + @Override + protected boolean shouldStopProcessing(final T currentToken) { + return currentToken == null; + } + + @Override + protected void analyzeTokens(final T currentToken, final Iterable remainingTokens) { + this.remainingTokens = remainingTokens; + } + + public Iterable getRemainingTokens() { + return remainingTokens; + } + } + + @Test + public void testRemainingTokensFunctionality1() { + final TokenManager tokenManager = new StringTokenManager(); + final DummyTokenFilter tokenFilter = new DummyTokenFilter(tokenManager); + final GenericToken firstToken = tokenFilter.getNextToken(); + assertEquals("a", firstToken.getImage()); + final Iterable iterable = tokenFilter.getRemainingTokens(); + final Iterator it1 = iterable.iterator(); + final Iterator it2 = iterable.iterator(); + assertTrue(it1.hasNext()); + assertTrue(it2.hasNext()); + final StringToken firstValFirstIt = (StringToken) it1.next(); + final StringToken firstValSecondIt = (StringToken) it2.next(); + assertTrue(it1.hasNext()); + assertTrue(it2.hasNext()); + final StringToken secondValFirstIt = (StringToken) it1.next(); + assertFalse(it1.hasNext()); + assertTrue(it2.hasNext()); + final StringToken secondValSecondIt = (StringToken) it2.next(); + assertFalse(it2.hasNext()); + assertEquals("b", firstValFirstIt.getImage()); + assertEquals("b", firstValSecondIt.getImage()); + assertEquals("c", secondValFirstIt.getImage()); + assertEquals("c", secondValSecondIt.getImage()); + } + + @Test + public void testRemainingTokensFunctionality2() { + final TokenManager tokenManager = new StringTokenManager(); + final DummyTokenFilter tokenFilter = new DummyTokenFilter(tokenManager); + final GenericToken firstToken = tokenFilter.getNextToken(); + assertEquals("a", firstToken.getImage()); + final Iterable iterable = tokenFilter.getRemainingTokens(); + final Iterator it1 = iterable.iterator(); + final Iterator it2 = iterable.iterator(); + assertTrue(it1.hasNext()); + assertTrue(it2.hasNext()); + final StringToken firstValFirstIt = (StringToken) it1.next(); + assertTrue(it1.hasNext()); + final StringToken secondValFirstIt = (StringToken) it1.next(); + assertFalse(it1.hasNext()); + assertTrue(it2.hasNext()); + final StringToken firstValSecondIt = (StringToken) it2.next(); + assertTrue(it2.hasNext()); + final StringToken secondValSecondIt = (StringToken) it2.next(); + assertFalse(it2.hasNext()); + assertEquals("b", firstValFirstIt.getImage()); + assertEquals("b", firstValSecondIt.getImage()); + assertEquals("c", secondValFirstIt.getImage()); + assertEquals("c", secondValSecondIt.getImage()); + } + + @Test(expected = NoSuchElementException.class) + public void testRemainingTokensFunctionality3() { + final TokenManager tokenManager = new StringTokenManager(); + final DummyTokenFilter tokenFilter = new DummyTokenFilter(tokenManager); + final GenericToken firstToken = tokenFilter.getNextToken(); + assertEquals("a", firstToken.getImage()); + final Iterable iterable = tokenFilter.getRemainingTokens(); + final Iterator it1 = iterable.iterator(); + final Iterator it2 = iterable.iterator(); + it1.next(); + it1.next(); + it2.next(); + it2.next(); + it1.next(); + } + + @Test(expected = ConcurrentModificationException.class) + public void testRemainingTokensFunctionality4() { + final TokenManager tokenManager = new StringTokenManager(); + final DummyTokenFilter tokenFilter = new DummyTokenFilter(tokenManager); + final GenericToken firstToken = tokenFilter.getNextToken(); + assertEquals("a", firstToken.getImage()); + final Iterable iterable = tokenFilter.getRemainingTokens(); + final Iterator it1 = iterable.iterator(); + final GenericToken secondToken = tokenFilter.getNextToken(); + assertEquals("b", secondToken.getImage()); + it1.next(); + } + +} diff --git a/pmd-cs/pom.xml b/pmd-cs/pom.xml index bcbcb3ce76..ce36e2dabc 100644 --- a/pmd-cs/pom.xml +++ b/pmd-cs/pom.xml @@ -12,6 +12,11 @@ + + org.antlr + antlr4-maven-plugin + + maven-resources-plugin @@ -23,6 +28,7 @@ + net.sourceforge.pmd @@ -32,10 +38,6 @@ commons-io commons-io - - org.apache.commons - commons-lang3 - junit diff --git a/pmd-cs/src/main/antlr4/net/sourceforge/pmd/lang/cs/antlr4/CSharpLexer.g4 b/pmd-cs/src/main/antlr4/net/sourceforge/pmd/lang/cs/antlr4/CSharpLexer.g4 new file mode 100644 index 0000000000..37428cbc72 --- /dev/null +++ b/pmd-cs/src/main/antlr4/net/sourceforge/pmd/lang/cs/antlr4/CSharpLexer.g4 @@ -0,0 +1,1105 @@ +// Eclipse Public License - v 1.0, http://www.eclipse.org/legal/epl-v10.html +// Copyright (c) 2013, Christian Wulf (chwchw@gmx.de) +// Copyright (c) 2016-2017, Ivan Kochurkin (kvanttt@gmail.com), Positive Technologies. + +lexer grammar CSharpLexer; + +@lexer::header +{import java.util.Stack;} + +channels { COMMENTS_CHANNEL, DIRECTIVE } + +@lexer::members +{private int interpolatedStringLevel; +private Stack interpolatedVerbatiums = new Stack(); +private Stack curlyLevels = new Stack(); +private boolean verbatium; +} + +BYTE_ORDER_MARK: '\u00EF\u00BB\u00BF'; + +SINGLE_LINE_DOC_COMMENT: '///' InputCharacter* -> channel(COMMENTS_CHANNEL); +DELIMITED_DOC_COMMENT: '/**' .*? '*/' -> channel(COMMENTS_CHANNEL); +SINGLE_LINE_COMMENT: '//' InputCharacter* -> channel(COMMENTS_CHANNEL); +DELIMITED_COMMENT: '/*' .*? '*/' -> channel(COMMENTS_CHANNEL); + +WHITESPACES: (Whitespace | NewLine)+ -> channel(HIDDEN); +SHARP: '#' -> mode(DIRECTIVE_MODE); + +ABSTRACT: 'abstract'; +ADD: 'add'; +ALIAS: 'alias'; +ARGLIST: '__arglist'; +AS: 'as'; +ASCENDING: 'ascending'; +ASYNC: 'async'; +AWAIT: 'await'; +BASE: 'base'; +BOOL: 'bool'; +BREAK: 'break'; +BY: 'by'; +BYTE: 'byte'; +CASE: 'case'; +CATCH: 'catch'; +CHAR: 'char'; +CHECKED: 'checked'; +CLASS: 'class'; +CONST: 'const'; +CONTINUE: 'continue'; +DECIMAL: 'decimal'; +DEFAULT: 'default'; +DELEGATE: 'delegate'; +DESCENDING: 'descending'; +DO: 'do'; +DOUBLE: 'double'; +DYNAMIC: 'dynamic'; +ELSE: 'else'; +ENUM: 'enum'; +EQUALS: 'equals'; +EVENT: 'event'; +EXPLICIT: 'explicit'; +EXTERN: 'extern'; +FALSE: 'false'; +FINALLY: 'finally'; +FIXED: 'fixed'; +FLOAT: 'float'; +FOR: 'for'; +FOREACH: 'foreach'; +FROM: 'from'; +GET: 'get'; +GOTO: 'goto'; +GROUP: 'group'; +IF: 'if'; +IMPLICIT: 'implicit'; +IN: 'in'; +INT: 'int'; +INTERFACE: 'interface'; +INTERNAL: 'internal'; +INTO: 'into'; +IS: 'is'; +JOIN: 'join'; +LET: 'let'; +LOCK: 'lock'; +LONG: 'long'; +NAMEOF: 'nameof'; +NAMESPACE: 'namespace'; +NEW: 'new'; +NULL: 'null'; +OBJECT: 'object'; +ON: 'on'; +OPERATOR: 'operator'; +ORDERBY: 'orderby'; +OUT: 'out'; +OVERRIDE: 'override'; +PARAMS: 'params'; +PARTIAL: 'partial'; +PRIVATE: 'private'; +PROTECTED: 'protected'; +PUBLIC: 'public'; +READONLY: 'readonly'; +REF: 'ref'; +REMOVE: 'remove'; +RETURN: 'return'; +SBYTE: 'sbyte'; +SEALED: 'sealed'; +SELECT: 'select'; +SET: 'set'; +SHORT: 'short'; +SIZEOF: 'sizeof'; +STACKALLOC: 'stackalloc'; +STATIC: 'static'; +STRING: 'string'; +STRUCT: 'struct'; +SWITCH: 'switch'; +THIS: 'this'; +THROW: 'throw'; +TRUE: 'true'; +TRY: 'try'; +TYPEOF: 'typeof'; +UINT: 'uint'; +ULONG: 'ulong'; +UNCHECKED: 'unchecked'; +UNSAFE: 'unsafe'; +USHORT: 'ushort'; +USING: 'using'; +VAR: 'var'; +VIRTUAL: 'virtual'; +VOID: 'void'; +VOLATILE: 'volatile'; +WHEN: 'when'; +WHERE: 'where'; +WHILE: 'while'; +YIELD: 'yield'; + +//B.1.6 Identifiers +// must be defined after all keywords so the first branch (Available_identifier) does not match keywords +// https://msdn.microsoft.com/en-us/library/aa664670(v=vs.71).aspx +IDENTIFIER: '@'? IdentifierOrKeyword; + +//B.1.8 Literals +// 0.Equals() would be parsed as an invalid real (1. branch) causing a lexer error +LITERAL_ACCESS: [0-9]+ IntegerTypeSuffix? '.' '@'? IdentifierOrKeyword; +INTEGER_LITERAL: [0-9]+ IntegerTypeSuffix?; +HEX_INTEGER_LITERAL: '0' [xX] HexDigit+ IntegerTypeSuffix?; +REAL_LITERAL: [0-9]* '.' [0-9]+ ExponentPart? [FfDdMm]? | [0-9]+ ([FfDdMm] | ExponentPart [FfDdMm]?); + +CHARACTER_LITERAL: '\'' (~['\\\r\n\u0085\u2028\u2029] | CommonCharacter) '\''; +REGULAR_STRING: '"' (~["\\\r\n\u0085\u2028\u2029] | CommonCharacter)* '"'; +VERBATIUM_STRING: '@"' (~'"' | '""')* '"'; +INTERPOLATED_REGULAR_STRING_START: '$"' + { interpolatedStringLevel++; interpolatedVerbatiums.push(false); verbatium = false; } -> pushMode(INTERPOLATION_STRING); +INTERPOLATED_VERBATIUM_STRING_START: '$@"' + { interpolatedStringLevel++; interpolatedVerbatiums.push(true); verbatium = true; } -> pushMode(INTERPOLATION_STRING); + +//B.1.9 Operators And Punctuators +OPEN_BRACE: '{' +{ +if (interpolatedStringLevel > 0) +{ + curlyLevels.push(curlyLevels.pop() + 1); +}}; +CLOSE_BRACE: '}' +{ +if (interpolatedStringLevel > 0) +{ + curlyLevels.push(curlyLevels.pop() - 1); + if (curlyLevels.peek() == 0) + { + curlyLevels.pop(); + skip(); + popMode(); + } +} +}; +OPEN_BRACKET: '['; +CLOSE_BRACKET: ']'; +OPEN_PARENS: '('; +CLOSE_PARENS: ')'; +DOT: '.'; +COMMA: ','; +COLON: ':' +{ +if (interpolatedStringLevel > 0) +{ + int ind = 1; + boolean switchToFormatString = true; + while ((char)_input.LA(ind) != '}') + { + if (_input.LA(ind) == ':' || _input.LA(ind) == ')') + { + switchToFormatString = false; + break; + } + ind++; + } + if (switchToFormatString) + { + mode(INTERPOLATION_FORMAT); + } +} +}; +SEMICOLON: ';'; +PLUS: '+'; +MINUS: '-'; +STAR: '*'; +DIV: '/'; +PERCENT: '%'; +AMP: '&'; +BITWISE_OR: '|'; +CARET: '^'; +BANG: '!'; +TILDE: '~'; +ASSIGNMENT: '='; +LT: '<'; +GT: '>'; +INTERR: '?'; +DOUBLE_COLON: '::'; +OP_COALESCING: '??'; +OP_INC: '++'; +OP_DEC: '--'; +OP_AND: '&&'; +OP_OR: '||'; +OP_PTR: '->'; +OP_EQ: '=='; +OP_NE: '!='; +OP_LE: '<='; +OP_GE: '>='; +OP_ADD_ASSIGNMENT: '+='; +OP_SUB_ASSIGNMENT: '-='; +OP_MULT_ASSIGNMENT: '*='; +OP_DIV_ASSIGNMENT: '/='; +OP_MOD_ASSIGNMENT: '%='; +OP_AND_ASSIGNMENT: '&='; +OP_OR_ASSIGNMENT: '|='; +OP_XOR_ASSIGNMENT: '^='; +OP_LEFT_SHIFT: '<<'; +OP_LEFT_SHIFT_ASSIGNMENT: '<<='; + +// https://msdn.microsoft.com/en-us/library/dn961160.aspx +mode INTERPOLATION_STRING; + +DOUBLE_CURLY_INSIDE: '{{'; +OPEN_BRACE_INSIDE: '{' { curlyLevels.push(1); } -> skip, pushMode(DEFAULT_MODE); +REGULAR_CHAR_INSIDE: { !verbatium }? SimpleEscapeSequence; +VERBATIUM_DOUBLE_QUOTE_INSIDE: { verbatium }? '""'; +DOUBLE_QUOTE_INSIDE: '"' { interpolatedStringLevel--; interpolatedVerbatiums.pop(); + verbatium = (interpolatedVerbatiums.size() > 0 ? interpolatedVerbatiums.peek() : false); } -> popMode; +REGULAR_STRING_INSIDE: { !verbatium }? ~('{' | '\\' | '"')+; +VERBATIUM_INSIDE_STRING: { verbatium }? ~('{' | '"')+; + +mode INTERPOLATION_FORMAT; + +DOUBLE_CURLY_CLOSE_INSIDE: '}}' -> type(FORMAT_STRING); +CLOSE_BRACE_INSIDE: '}' { curlyLevels.pop(); } -> skip, popMode; +FORMAT_STRING: ~'}'+; + +mode DIRECTIVE_MODE; + +DIRECTIVE_WHITESPACES: Whitespace+ -> channel(HIDDEN); +DIGITS: [0-9]+ -> channel(DIRECTIVE); +DIRECTIVE_TRUE: 'true' -> channel(DIRECTIVE), type(TRUE); +DIRECTIVE_FALSE: 'false' -> channel(DIRECTIVE), type(FALSE); +DEFINE: 'define' -> channel(DIRECTIVE); +UNDEF: 'undef' -> channel(DIRECTIVE); +DIRECTIVE_IF: 'if' -> channel(DIRECTIVE), type(IF); +ELIF: 'elif' -> channel(DIRECTIVE); +DIRECTIVE_ELSE: 'else' -> channel(DIRECTIVE), type(ELSE); +ENDIF: 'endif' -> channel(DIRECTIVE); +LINE: 'line' -> channel(DIRECTIVE); +ERROR: 'error' Whitespace+ -> channel(DIRECTIVE), mode(DIRECTIVE_TEXT); +WARNING: 'warning' Whitespace+ -> channel(DIRECTIVE), mode(DIRECTIVE_TEXT); +REGION: 'region' Whitespace* -> channel(DIRECTIVE), mode(DIRECTIVE_TEXT); +ENDREGION: 'endregion' Whitespace* -> channel(DIRECTIVE), mode(DIRECTIVE_TEXT); +PRAGMA: 'pragma' Whitespace+ -> channel(DIRECTIVE), mode(DIRECTIVE_TEXT); +DIRECTIVE_DEFAULT: 'default' -> channel(DIRECTIVE), type(DEFAULT); +DIRECTIVE_HIDDEN: 'hidden' -> channel(DIRECTIVE); +DIRECTIVE_OPEN_PARENS: '(' -> channel(DIRECTIVE), type(OPEN_PARENS); +DIRECTIVE_CLOSE_PARENS: ')' -> channel(DIRECTIVE), type(CLOSE_PARENS); +DIRECTIVE_BANG: '!' -> channel(DIRECTIVE), type(BANG); +DIRECTIVE_OP_EQ: '==' -> channel(DIRECTIVE), type(OP_EQ); +DIRECTIVE_OP_NE: '!=' -> channel(DIRECTIVE), type(OP_NE); +DIRECTIVE_OP_AND: '&&' -> channel(DIRECTIVE), type(OP_AND); +DIRECTIVE_OP_OR: '||' -> channel(DIRECTIVE), type(OP_OR); +DIRECTIVE_STRING: '"' ~('"' | [\r\n\u0085\u2028\u2029])* '"' -> channel(DIRECTIVE), type(STRING); +CONDITIONAL_SYMBOL: IdentifierOrKeyword -> channel(DIRECTIVE); +DIRECTIVE_SINGLE_LINE_COMMENT: '//' ~[\r\n\u0085\u2028\u2029]* -> channel(COMMENTS_CHANNEL), type(SINGLE_LINE_COMMENT); +DIRECTIVE_NEW_LINE: NewLine -> channel(DIRECTIVE), mode(DEFAULT_MODE); + +mode DIRECTIVE_TEXT; + +TEXT: ~[\r\n\u0085\u2028\u2029]+ -> channel(DIRECTIVE); +TEXT_NEW_LINE: NewLine -> channel(DIRECTIVE), type(DIRECTIVE_NEW_LINE), mode(DEFAULT_MODE); + +// Fragments + +fragment InputCharacter: ~[\r\n\u0085\u2028\u2029]; + +fragment NewLineCharacter + : '\u000D' //'' + | '\u000A' //'' + | '\u0085' //'' + | '\u2028' //'' + | '\u2029' //'' + ; + +fragment IntegerTypeSuffix: [lL]? [uU] | [uU]? [lL]; +fragment ExponentPart: [eE] ('+' | '-')? [0-9]+; + +fragment CommonCharacter + : SimpleEscapeSequence + | HexEscapeSequence + | UnicodeEscapeSequence + ; + +fragment SimpleEscapeSequence + : '\\\'' + | '\\"' + | '\\\\' + | '\\0' + | '\\a' + | '\\b' + | '\\f' + | '\\n' + | '\\r' + | '\\t' + | '\\v' + ; + +fragment HexEscapeSequence + : '\\x' HexDigit + | '\\x' HexDigit HexDigit + | '\\x' HexDigit HexDigit HexDigit + | '\\x' HexDigit HexDigit HexDigit HexDigit + ; + +fragment NewLine + : NL + ; + +NL + : '\r\n' | '\r' | '\n' + | '\u0085' // ' + | '\u2028' //'' + | '\u2029' //'' + ; + +fragment Whitespace + : UnicodeClassZS //'' + | '\u0009' //'' + | '\u000B' //'' + | '\u000C' //'
' + ; + +fragment UnicodeClassZS + : '\u0020' // SPACE + | '\u00A0' // NO_BREAK SPACE + | '\u1680' // OGHAM SPACE MARK + | '\u180E' // MONGOLIAN VOWEL SEPARATOR + | '\u2000' // EN QUAD + | '\u2001' // EM QUAD + | '\u2002' // EN SPACE + | '\u2003' // EM SPACE + | '\u2004' // THREE_PER_EM SPACE + | '\u2005' // FOUR_PER_EM SPACE + | '\u2006' // SIX_PER_EM SPACE + | '\u2008' // PUNCTUATION SPACE + | '\u2009' // THIN SPACE + | '\u200A' // HAIR SPACE + | '\u202F' // NARROW NO_BREAK SPACE + | '\u3000' // IDEOGRAPHIC SPACE + | '\u205F' // MEDIUM MATHEMATICAL SPACE + ; + +fragment IdentifierOrKeyword + : IdentifierStartCharacter IdentifierPartCharacter* + ; + +fragment IdentifierStartCharacter + : LetterCharacter + | '_' + ; + +fragment IdentifierPartCharacter + : LetterCharacter + | DecimalDigitCharacter + | ConnectingCharacter + | CombiningCharacter + | FormattingCharacter + ; + +//'' +// WARNING: ignores UnicodeEscapeSequence +fragment LetterCharacter + : UnicodeClassLU + | UnicodeClassLL + | UnicodeClassLT + | UnicodeClassLM + | UnicodeClassLO + | UnicodeClassNL + | UnicodeEscapeSequence + ; + +//'' +// WARNING: ignores UnicodeEscapeSequence +fragment DecimalDigitCharacter + : UnicodeClassND + | UnicodeEscapeSequence + ; + +//'' +// WARNING: ignores UnicodeEscapeSequence +fragment ConnectingCharacter + : UnicodeClassPC + | UnicodeEscapeSequence + ; + +//'' +// WARNING: ignores UnicodeEscapeSequence +fragment CombiningCharacter + : UnicodeClassMN + | UnicodeClassMC + | UnicodeEscapeSequence + ; + +//'' +// WARNING: ignores UnicodeEscapeSequence +fragment FormattingCharacter + : UnicodeClassCF + | UnicodeEscapeSequence + ; + +//B.1.5 Unicode Character Escape Sequences +fragment UnicodeEscapeSequence + : '\\u' HexDigit HexDigit HexDigit HexDigit + | '\\U' HexDigit HexDigit HexDigit HexDigit HexDigit HexDigit HexDigit HexDigit + ; + +fragment HexDigit : [0-9] | [A-F] | [a-f]; + +// Unicode character classes +fragment UnicodeClassLU + : '\u0041'..'\u005a' + | '\u00c0'..'\u00d6' + | '\u00d8'..'\u00de' + | '\u0100'..'\u0136' + | '\u0139'..'\u0147' + | '\u014a'..'\u0178' + | '\u0179'..'\u017d' + | '\u0181'..'\u0182' + | '\u0184'..'\u0186' + | '\u0187'..'\u0189' + | '\u018a'..'\u018b' + | '\u018e'..'\u0191' + | '\u0193'..'\u0194' + | '\u0196'..'\u0198' + | '\u019c'..'\u019d' + | '\u019f'..'\u01a0' + | '\u01a2'..'\u01a6' + | '\u01a7'..'\u01a9' + | '\u01ac'..'\u01ae' + | '\u01af'..'\u01b1' + | '\u01b2'..'\u01b3' + | '\u01b5'..'\u01b7' + | '\u01b8'..'\u01bc' + | '\u01c4'..'\u01cd' + | '\u01cf'..'\u01db' + | '\u01de'..'\u01ee' + | '\u01f1'..'\u01f4' + | '\u01f6'..'\u01f8' + | '\u01fa'..'\u0232' + | '\u023a'..'\u023b' + | '\u023d'..'\u023e' + | '\u0241'..'\u0243' + | '\u0244'..'\u0246' + | '\u0248'..'\u024e' + | '\u0370'..'\u0372' + | '\u0376'..'\u037f' + | '\u0386'..'\u0388' + | '\u0389'..'\u038a' + | '\u038c'..'\u038e' + | '\u038f'..'\u0391' + | '\u0392'..'\u03a1' + | '\u03a3'..'\u03ab' + | '\u03cf'..'\u03d2' + | '\u03d3'..'\u03d4' + | '\u03d8'..'\u03ee' + | '\u03f4'..'\u03f7' + | '\u03f9'..'\u03fa' + | '\u03fd'..'\u042f' + | '\u0460'..'\u0480' + | '\u048a'..'\u04c0' + | '\u04c1'..'\u04cd' + | '\u04d0'..'\u052e' + | '\u0531'..'\u0556' + | '\u10a0'..'\u10c5' + | '\u10c7'..'\u10cd' + | '\u1e00'..'\u1e94' + | '\u1e9e'..'\u1efe' + | '\u1f08'..'\u1f0f' + | '\u1f18'..'\u1f1d' + | '\u1f28'..'\u1f2f' + | '\u1f38'..'\u1f3f' + | '\u1f48'..'\u1f4d' + | '\u1f59'..'\u1f5f' + | '\u1f68'..'\u1f6f' + | '\u1fb8'..'\u1fbb' + | '\u1fc8'..'\u1fcb' + | '\u1fd8'..'\u1fdb' + | '\u1fe8'..'\u1fec' + | '\u1ff8'..'\u1ffb' + | '\u2102'..'\u2107' + | '\u210b'..'\u210d' + | '\u2110'..'\u2112' + | '\u2115'..'\u2119' + | '\u211a'..'\u211d' + | '\u2124'..'\u212a' + | '\u212b'..'\u212d' + | '\u2130'..'\u2133' + | '\u213e'..'\u213f' + | '\u2145'..'\u2183' + | '\u2c00'..'\u2c2e' + | '\u2c60'..'\u2c62' + | '\u2c63'..'\u2c64' + | '\u2c67'..'\u2c6d' + | '\u2c6e'..'\u2c70' + | '\u2c72'..'\u2c75' + | '\u2c7e'..'\u2c80' + | '\u2c82'..'\u2ce2' + | '\u2ceb'..'\u2ced' + | '\u2cf2'..'\ua640' + | '\ua642'..'\ua66c' + | '\ua680'..'\ua69a' + | '\ua722'..'\ua72e' + | '\ua732'..'\ua76e' + | '\ua779'..'\ua77d' + | '\ua77e'..'\ua786' + | '\ua78b'..'\ua78d' + | '\ua790'..'\ua792' + | '\ua796'..'\ua7aa' + | '\ua7ab'..'\ua7ad' + | '\ua7b0'..'\ua7b1' + | '\uff21'..'\uff3a' + ; + +fragment UnicodeClassLL + : '\u0061'..'\u007A' + | '\u00b5'..'\u00df' + | '\u00e0'..'\u00f6' + | '\u00f8'..'\u00ff' + | '\u0101'..'\u0137' + | '\u0138'..'\u0148' + | '\u0149'..'\u0177' + | '\u017a'..'\u017e' + | '\u017f'..'\u0180' + | '\u0183'..'\u0185' + | '\u0188'..'\u018c' + | '\u018d'..'\u0192' + | '\u0195'..'\u0199' + | '\u019a'..'\u019b' + | '\u019e'..'\u01a1' + | '\u01a3'..'\u01a5' + | '\u01a8'..'\u01aa' + | '\u01ab'..'\u01ad' + | '\u01b0'..'\u01b4' + | '\u01b6'..'\u01b9' + | '\u01ba'..'\u01bd' + | '\u01be'..'\u01bf' + | '\u01c6'..'\u01cc' + | '\u01ce'..'\u01dc' + | '\u01dd'..'\u01ef' + | '\u01f0'..'\u01f3' + | '\u01f5'..'\u01f9' + | '\u01fb'..'\u0233' + | '\u0234'..'\u0239' + | '\u023c'..'\u023f' + | '\u0240'..'\u0242' + | '\u0247'..'\u024f' + | '\u0250'..'\u0293' + | '\u0295'..'\u02af' + | '\u0371'..'\u0373' + | '\u0377'..'\u037b' + | '\u037c'..'\u037d' + | '\u0390'..'\u03ac' + | '\u03ad'..'\u03ce' + | '\u03d0'..'\u03d1' + | '\u03d5'..'\u03d7' + | '\u03d9'..'\u03ef' + | '\u03f0'..'\u03f3' + | '\u03f5'..'\u03fb' + | '\u03fc'..'\u0430' + | '\u0431'..'\u045f' + | '\u0461'..'\u0481' + | '\u048b'..'\u04bf' + | '\u04c2'..'\u04ce' + | '\u04cf'..'\u052f' + | '\u0561'..'\u0587' + | '\u1d00'..'\u1d2b' + | '\u1d6b'..'\u1d77' + | '\u1d79'..'\u1d9a' + | '\u1e01'..'\u1e95' + | '\u1e96'..'\u1e9d' + | '\u1e9f'..'\u1eff' + | '\u1f00'..'\u1f07' + | '\u1f10'..'\u1f15' + | '\u1f20'..'\u1f27' + | '\u1f30'..'\u1f37' + | '\u1f40'..'\u1f45' + | '\u1f50'..'\u1f57' + | '\u1f60'..'\u1f67' + | '\u1f70'..'\u1f7d' + | '\u1f80'..'\u1f87' + | '\u1f90'..'\u1f97' + | '\u1fa0'..'\u1fa7' + | '\u1fb0'..'\u1fb4' + | '\u1fb6'..'\u1fb7' + | '\u1fbe'..'\u1fc2' + | '\u1fc3'..'\u1fc4' + | '\u1fc6'..'\u1fc7' + | '\u1fd0'..'\u1fd3' + | '\u1fd6'..'\u1fd7' + | '\u1fe0'..'\u1fe7' + | '\u1ff2'..'\u1ff4' + | '\u1ff6'..'\u1ff7' + | '\u210a'..'\u210e' + | '\u210f'..'\u2113' + | '\u212f'..'\u2139' + | '\u213c'..'\u213d' + | '\u2146'..'\u2149' + | '\u214e'..'\u2184' + | '\u2c30'..'\u2c5e' + | '\u2c61'..'\u2c65' + | '\u2c66'..'\u2c6c' + | '\u2c71'..'\u2c73' + | '\u2c74'..'\u2c76' + | '\u2c77'..'\u2c7b' + | '\u2c81'..'\u2ce3' + | '\u2ce4'..'\u2cec' + | '\u2cee'..'\u2cf3' + | '\u2d00'..'\u2d25' + | '\u2d27'..'\u2d2d' + | '\ua641'..'\ua66d' + | '\ua681'..'\ua69b' + | '\ua723'..'\ua72f' + | '\ua730'..'\ua731' + | '\ua733'..'\ua771' + | '\ua772'..'\ua778' + | '\ua77a'..'\ua77c' + | '\ua77f'..'\ua787' + | '\ua78c'..'\ua78e' + | '\ua791'..'\ua793' + | '\ua794'..'\ua795' + | '\ua797'..'\ua7a9' + | '\ua7fa'..'\uab30' + | '\uab31'..'\uab5a' + | '\uab64'..'\uab65' + | '\ufb00'..'\ufb06' + | '\ufb13'..'\ufb17' + | '\uff41'..'\uff5a' + ; + +fragment UnicodeClassLT + : '\u01c5'..'\u01cb' + | '\u01f2'..'\u1f88' + | '\u1f89'..'\u1f8f' + | '\u1f98'..'\u1f9f' + | '\u1fa8'..'\u1faf' + | '\u1fbc'..'\u1fcc' + | '\u1ffc'..'\u1ffc' + ; + +fragment UnicodeClassLM + : '\u02b0'..'\u02c1' + | '\u02c6'..'\u02d1' + | '\u02e0'..'\u02e4' + | '\u02ec'..'\u02ee' + | '\u0374'..'\u037a' + | '\u0559'..'\u0640' + | '\u06e5'..'\u06e6' + | '\u07f4'..'\u07f5' + | '\u07fa'..'\u081a' + | '\u0824'..'\u0828' + | '\u0971'..'\u0e46' + | '\u0ec6'..'\u10fc' + | '\u17d7'..'\u1843' + | '\u1aa7'..'\u1c78' + | '\u1c79'..'\u1c7d' + | '\u1d2c'..'\u1d6a' + | '\u1d78'..'\u1d9b' + | '\u1d9c'..'\u1dbf' + | '\u2071'..'\u207f' + | '\u2090'..'\u209c' + | '\u2c7c'..'\u2c7d' + | '\u2d6f'..'\u2e2f' + | '\u3005'..'\u3031' + | '\u3032'..'\u3035' + | '\u303b'..'\u309d' + | '\u309e'..'\u30fc' + | '\u30fd'..'\u30fe' + | '\ua015'..'\ua4f8' + | '\ua4f9'..'\ua4fd' + | '\ua60c'..'\ua67f' + | '\ua69c'..'\ua69d' + | '\ua717'..'\ua71f' + | '\ua770'..'\ua788' + | '\ua7f8'..'\ua7f9' + | '\ua9cf'..'\ua9e6' + | '\uaa70'..'\uaadd' + | '\uaaf3'..'\uaaf4' + | '\uab5c'..'\uab5f' + | '\uff70'..'\uff9e' + | '\uff9f'..'\uff9f' + ; + +fragment UnicodeClassLO + : '\u00aa'..'\u00ba' + | '\u01bb'..'\u01c0' + | '\u01c1'..'\u01c3' + | '\u0294'..'\u05d0' + | '\u05d1'..'\u05ea' + | '\u05f0'..'\u05f2' + | '\u0620'..'\u063f' + | '\u0641'..'\u064a' + | '\u066e'..'\u066f' + | '\u0671'..'\u06d3' + | '\u06d5'..'\u06ee' + | '\u06ef'..'\u06fa' + | '\u06fb'..'\u06fc' + | '\u06ff'..'\u0710' + | '\u0712'..'\u072f' + | '\u074d'..'\u07a5' + | '\u07b1'..'\u07ca' + | '\u07cb'..'\u07ea' + | '\u0800'..'\u0815' + | '\u0840'..'\u0858' + | '\u08a0'..'\u08b2' + | '\u0904'..'\u0939' + | '\u093d'..'\u0950' + | '\u0958'..'\u0961' + | '\u0972'..'\u0980' + | '\u0985'..'\u098c' + | '\u098f'..'\u0990' + | '\u0993'..'\u09a8' + | '\u09aa'..'\u09b0' + | '\u09b2'..'\u09b6' + | '\u09b7'..'\u09b9' + | '\u09bd'..'\u09ce' + | '\u09dc'..'\u09dd' + | '\u09df'..'\u09e1' + | '\u09f0'..'\u09f1' + | '\u0a05'..'\u0a0a' + | '\u0a0f'..'\u0a10' + | '\u0a13'..'\u0a28' + | '\u0a2a'..'\u0a30' + | '\u0a32'..'\u0a33' + | '\u0a35'..'\u0a36' + | '\u0a38'..'\u0a39' + | '\u0a59'..'\u0a5c' + | '\u0a5e'..'\u0a72' + | '\u0a73'..'\u0a74' + | '\u0a85'..'\u0a8d' + | '\u0a8f'..'\u0a91' + | '\u0a93'..'\u0aa8' + | '\u0aaa'..'\u0ab0' + | '\u0ab2'..'\u0ab3' + | '\u0ab5'..'\u0ab9' + | '\u0abd'..'\u0ad0' + | '\u0ae0'..'\u0ae1' + | '\u0b05'..'\u0b0c' + | '\u0b0f'..'\u0b10' + | '\u0b13'..'\u0b28' + | '\u0b2a'..'\u0b30' + | '\u0b32'..'\u0b33' + | '\u0b35'..'\u0b39' + | '\u0b3d'..'\u0b5c' + | '\u0b5d'..'\u0b5f' + | '\u0b60'..'\u0b61' + | '\u0b71'..'\u0b83' + | '\u0b85'..'\u0b8a' + | '\u0b8e'..'\u0b90' + | '\u0b92'..'\u0b95' + | '\u0b99'..'\u0b9a' + | '\u0b9c'..'\u0b9e' + | '\u0b9f'..'\u0ba3' + | '\u0ba4'..'\u0ba8' + | '\u0ba9'..'\u0baa' + | '\u0bae'..'\u0bb9' + | '\u0bd0'..'\u0c05' + | '\u0c06'..'\u0c0c' + | '\u0c0e'..'\u0c10' + | '\u0c12'..'\u0c28' + | '\u0c2a'..'\u0c39' + | '\u0c3d'..'\u0c58' + | '\u0c59'..'\u0c60' + | '\u0c61'..'\u0c85' + | '\u0c86'..'\u0c8c' + | '\u0c8e'..'\u0c90' + | '\u0c92'..'\u0ca8' + | '\u0caa'..'\u0cb3' + | '\u0cb5'..'\u0cb9' + | '\u0cbd'..'\u0cde' + | '\u0ce0'..'\u0ce1' + | '\u0cf1'..'\u0cf2' + | '\u0d05'..'\u0d0c' + | '\u0d0e'..'\u0d10' + | '\u0d12'..'\u0d3a' + | '\u0d3d'..'\u0d4e' + | '\u0d60'..'\u0d61' + | '\u0d7a'..'\u0d7f' + | '\u0d85'..'\u0d96' + | '\u0d9a'..'\u0db1' + | '\u0db3'..'\u0dbb' + | '\u0dbd'..'\u0dc0' + | '\u0dc1'..'\u0dc6' + | '\u0e01'..'\u0e30' + | '\u0e32'..'\u0e33' + | '\u0e40'..'\u0e45' + | '\u0e81'..'\u0e82' + | '\u0e84'..'\u0e87' + | '\u0e88'..'\u0e8a' + | '\u0e8d'..'\u0e94' + | '\u0e95'..'\u0e97' + | '\u0e99'..'\u0e9f' + | '\u0ea1'..'\u0ea3' + | '\u0ea5'..'\u0ea7' + | '\u0eaa'..'\u0eab' + | '\u0ead'..'\u0eb0' + | '\u0eb2'..'\u0eb3' + | '\u0ebd'..'\u0ec0' + | '\u0ec1'..'\u0ec4' + | '\u0edc'..'\u0edf' + | '\u0f00'..'\u0f40' + | '\u0f41'..'\u0f47' + | '\u0f49'..'\u0f6c' + | '\u0f88'..'\u0f8c' + | '\u1000'..'\u102a' + | '\u103f'..'\u1050' + | '\u1051'..'\u1055' + | '\u105a'..'\u105d' + | '\u1061'..'\u1065' + | '\u1066'..'\u106e' + | '\u106f'..'\u1070' + | '\u1075'..'\u1081' + | '\u108e'..'\u10d0' + | '\u10d1'..'\u10fa' + | '\u10fd'..'\u1248' + | '\u124a'..'\u124d' + | '\u1250'..'\u1256' + | '\u1258'..'\u125a' + | '\u125b'..'\u125d' + | '\u1260'..'\u1288' + | '\u128a'..'\u128d' + | '\u1290'..'\u12b0' + | '\u12b2'..'\u12b5' + | '\u12b8'..'\u12be' + | '\u12c0'..'\u12c2' + | '\u12c3'..'\u12c5' + | '\u12c8'..'\u12d6' + | '\u12d8'..'\u1310' + | '\u1312'..'\u1315' + | '\u1318'..'\u135a' + | '\u1380'..'\u138f' + | '\u13a0'..'\u13f4' + | '\u1401'..'\u166c' + | '\u166f'..'\u167f' + | '\u1681'..'\u169a' + | '\u16a0'..'\u16ea' + | '\u16f1'..'\u16f8' + | '\u1700'..'\u170c' + | '\u170e'..'\u1711' + | '\u1720'..'\u1731' + | '\u1740'..'\u1751' + | '\u1760'..'\u176c' + | '\u176e'..'\u1770' + | '\u1780'..'\u17b3' + | '\u17dc'..'\u1820' + | '\u1821'..'\u1842' + | '\u1844'..'\u1877' + | '\u1880'..'\u18a8' + | '\u18aa'..'\u18b0' + | '\u18b1'..'\u18f5' + | '\u1900'..'\u191e' + | '\u1950'..'\u196d' + | '\u1970'..'\u1974' + | '\u1980'..'\u19ab' + | '\u19c1'..'\u19c7' + | '\u1a00'..'\u1a16' + | '\u1a20'..'\u1a54' + | '\u1b05'..'\u1b33' + | '\u1b45'..'\u1b4b' + | '\u1b83'..'\u1ba0' + | '\u1bae'..'\u1baf' + | '\u1bba'..'\u1be5' + | '\u1c00'..'\u1c23' + | '\u1c4d'..'\u1c4f' + | '\u1c5a'..'\u1c77' + | '\u1ce9'..'\u1cec' + | '\u1cee'..'\u1cf1' + | '\u1cf5'..'\u1cf6' + | '\u2135'..'\u2138' + | '\u2d30'..'\u2d67' + | '\u2d80'..'\u2d96' + | '\u2da0'..'\u2da6' + | '\u2da8'..'\u2dae' + | '\u2db0'..'\u2db6' + | '\u2db8'..'\u2dbe' + | '\u2dc0'..'\u2dc6' + | '\u2dc8'..'\u2dce' + | '\u2dd0'..'\u2dd6' + | '\u2dd8'..'\u2dde' + | '\u3006'..'\u303c' + | '\u3041'..'\u3096' + | '\u309f'..'\u30a1' + | '\u30a2'..'\u30fa' + | '\u30ff'..'\u3105' + | '\u3106'..'\u312d' + | '\u3131'..'\u318e' + | '\u31a0'..'\u31ba' + | '\u31f0'..'\u31ff' + | '\u3400'..'\u4db5' + | '\u4e00'..'\u9fcc' + | '\ua000'..'\ua014' + | '\ua016'..'\ua48c' + | '\ua4d0'..'\ua4f7' + | '\ua500'..'\ua60b' + | '\ua610'..'\ua61f' + | '\ua62a'..'\ua62b' + | '\ua66e'..'\ua6a0' + | '\ua6a1'..'\ua6e5' + | '\ua7f7'..'\ua7fb' + | '\ua7fc'..'\ua801' + | '\ua803'..'\ua805' + | '\ua807'..'\ua80a' + | '\ua80c'..'\ua822' + | '\ua840'..'\ua873' + | '\ua882'..'\ua8b3' + | '\ua8f2'..'\ua8f7' + | '\ua8fb'..'\ua90a' + | '\ua90b'..'\ua925' + | '\ua930'..'\ua946' + | '\ua960'..'\ua97c' + | '\ua984'..'\ua9b2' + | '\ua9e0'..'\ua9e4' + | '\ua9e7'..'\ua9ef' + | '\ua9fa'..'\ua9fe' + | '\uaa00'..'\uaa28' + | '\uaa40'..'\uaa42' + | '\uaa44'..'\uaa4b' + | '\uaa60'..'\uaa6f' + | '\uaa71'..'\uaa76' + | '\uaa7a'..'\uaa7e' + | '\uaa7f'..'\uaaaf' + | '\uaab1'..'\uaab5' + | '\uaab6'..'\uaab9' + | '\uaaba'..'\uaabd' + | '\uaac0'..'\uaac2' + | '\uaadb'..'\uaadc' + | '\uaae0'..'\uaaea' + | '\uaaf2'..'\uab01' + | '\uab02'..'\uab06' + | '\uab09'..'\uab0e' + | '\uab11'..'\uab16' + | '\uab20'..'\uab26' + | '\uab28'..'\uab2e' + | '\uabc0'..'\uabe2' + | '\uac00'..'\ud7a3' + | '\ud7b0'..'\ud7c6' + | '\ud7cb'..'\ud7fb' + | '\uf900'..'\ufa6d' + | '\ufa70'..'\ufad9' + | '\ufb1d'..'\ufb1f' + | '\ufb20'..'\ufb28' + | '\ufb2a'..'\ufb36' + | '\ufb38'..'\ufb3c' + | '\ufb3e'..'\ufb40' + | '\ufb41'..'\ufb43' + | '\ufb44'..'\ufb46' + | '\ufb47'..'\ufbb1' + | '\ufbd3'..'\ufd3d' + | '\ufd50'..'\ufd8f' + | '\ufd92'..'\ufdc7' + | '\ufdf0'..'\ufdfb' + | '\ufe70'..'\ufe74' + | '\ufe76'..'\ufefc' + | '\uff66'..'\uff6f' + | '\uff71'..'\uff9d' + | '\uffa0'..'\uffbe' + | '\uffc2'..'\uffc7' + | '\uffca'..'\uffcf' + | '\uffd2'..'\uffd7' + | '\uffda'..'\uffdc' + ; + +fragment UnicodeClassNL + : '\u16EE' // RUNIC ARLAUG SYMBOL + | '\u16EF' // RUNIC TVIMADUR SYMBOL + | '\u16F0' // RUNIC BELGTHOR SYMBOL + | '\u2160' // ROMAN NUMERAL ONE + | '\u2161' // ROMAN NUMERAL TWO + | '\u2162' // ROMAN NUMERAL THREE + | '\u2163' // ROMAN NUMERAL FOUR + | '\u2164' // ROMAN NUMERAL FIVE + | '\u2165' // ROMAN NUMERAL SIX + | '\u2166' // ROMAN NUMERAL SEVEN + | '\u2167' // ROMAN NUMERAL EIGHT + | '\u2168' // ROMAN NUMERAL NINE + | '\u2169' // ROMAN NUMERAL TEN + | '\u216A' // ROMAN NUMERAL ELEVEN + | '\u216B' // ROMAN NUMERAL TWELVE + | '\u216C' // ROMAN NUMERAL FIFTY + | '\u216D' // ROMAN NUMERAL ONE HUNDRED + | '\u216E' // ROMAN NUMERAL FIVE HUNDRED + | '\u216F' // ROMAN NUMERAL ONE THOUSAND + ; + +fragment UnicodeClassMN + : '\u0300' // COMBINING GRAVE ACCENT + | '\u0301' // COMBINING ACUTE ACCENT + | '\u0302' // COMBINING CIRCUMFLEX ACCENT + | '\u0303' // COMBINING TILDE + | '\u0304' // COMBINING MACRON + | '\u0305' // COMBINING OVERLINE + | '\u0306' // COMBINING BREVE + | '\u0307' // COMBINING DOT ABOVE + | '\u0308' // COMBINING DIAERESIS + | '\u0309' // COMBINING HOOK ABOVE + | '\u030A' // COMBINING RING ABOVE + | '\u030B' // COMBINING DOUBLE ACUTE ACCENT + | '\u030C' // COMBINING CARON + | '\u030D' // COMBINING VERTICAL LINE ABOVE + | '\u030E' // COMBINING DOUBLE VERTICAL LINE ABOVE + | '\u030F' // COMBINING DOUBLE GRAVE ACCENT + | '\u0310' // COMBINING CANDRABINDU + ; + +fragment UnicodeClassMC + : '\u0903' // DEVANAGARI SIGN VISARGA + | '\u093E' // DEVANAGARI VOWEL SIGN AA + | '\u093F' // DEVANAGARI VOWEL SIGN I + | '\u0940' // DEVANAGARI VOWEL SIGN II + | '\u0949' // DEVANAGARI VOWEL SIGN CANDRA O + | '\u094A' // DEVANAGARI VOWEL SIGN SHORT O + | '\u094B' // DEVANAGARI VOWEL SIGN O + | '\u094C' // DEVANAGARI VOWEL SIGN AU + ; + +fragment UnicodeClassCF + : '\u00AD' // SOFT HYPHEN + | '\u0600' // ARABIC NUMBER SIGN + | '\u0601' // ARABIC SIGN SANAH + | '\u0602' // ARABIC FOOTNOTE MARKER + | '\u0603' // ARABIC SIGN SAFHA + | '\u06DD' // ARABIC END OF AYAH + ; + +fragment UnicodeClassPC + : '\u005F' // LOW LINE + | '\u203F' // UNDERTIE + | '\u2040' // CHARACTER TIE + | '\u2054' // INVERTED UNDERTIE + | '\uFE33' // PRESENTATION FORM FOR VERTICAL LOW LINE + | '\uFE34' // PRESENTATION FORM FOR VERTICAL WAVY LOW LINE + | '\uFE4D' // DASHED LOW LINE + | '\uFE4E' // CENTRELINE LOW LINE + | '\uFE4F' // WAVY LOW LINE + | '\uFF3F' // FULLWIDTH LOW LINE + ; + +fragment UnicodeClassND + : '\u0030'..'\u0039' + | '\u0660'..'\u0669' + | '\u06f0'..'\u06f9' + | '\u07c0'..'\u07c9' + | '\u0966'..'\u096f' + | '\u09e6'..'\u09ef' + | '\u0a66'..'\u0a6f' + | '\u0ae6'..'\u0aef' + | '\u0b66'..'\u0b6f' + | '\u0be6'..'\u0bef' + | '\u0c66'..'\u0c6f' + | '\u0ce6'..'\u0cef' + | '\u0d66'..'\u0d6f' + | '\u0de6'..'\u0def' + | '\u0e50'..'\u0e59' + | '\u0ed0'..'\u0ed9' + | '\u0f20'..'\u0f29' + | '\u1040'..'\u1049' + | '\u1090'..'\u1099' + | '\u17e0'..'\u17e9' + | '\u1810'..'\u1819' + | '\u1946'..'\u194f' + | '\u19d0'..'\u19d9' + | '\u1a80'..'\u1a89' + | '\u1a90'..'\u1a99' + | '\u1b50'..'\u1b59' + | '\u1bb0'..'\u1bb9' + | '\u1c40'..'\u1c49' + | '\u1c50'..'\u1c59' + | '\ua620'..'\ua629' + | '\ua8d0'..'\ua8d9' + | '\ua900'..'\ua909' + | '\ua9d0'..'\ua9d9' + | '\ua9f0'..'\ua9f9' + | '\uaa50'..'\uaa59' + | '\uabf0'..'\uabf9' + | '\uff10'..'\uff19' + ; \ No newline at end of file diff --git a/pmd-cs/src/main/java/net/sourceforge/pmd/cpd/CsTokenizer.java b/pmd-cs/src/main/java/net/sourceforge/pmd/cpd/CsTokenizer.java index c3e4f6e622..9978cb4d93 100644 --- a/pmd-cs/src/main/java/net/sourceforge/pmd/cpd/CsTokenizer.java +++ b/pmd-cs/src/main/java/net/sourceforge/pmd/cpd/CsTokenizer.java @@ -4,21 +4,20 @@ package net.sourceforge.pmd.cpd; -import java.io.BufferedReader; -import java.io.CharArrayReader; -import java.io.Closeable; -import java.io.IOException; -import java.io.PushbackReader; import java.util.Properties; -import org.apache.commons.lang3.RandomStringUtils; +import org.antlr.v4.runtime.CharStream; + +import net.sourceforge.pmd.cpd.internal.AntlrTokenizer; +import net.sourceforge.pmd.cpd.token.AntlrToken; +import net.sourceforge.pmd.cpd.token.AntlrTokenFilter; +import net.sourceforge.pmd.lang.ast.impl.antlr4.AntlrTokenManager; +import net.sourceforge.pmd.lang.cs.antlr4.CSharpLexer; /** - * This class does a best-guess try-anything tokenization. - * - * @author jheintz + * The C# tokenizer. */ -public class CsTokenizer implements Tokenizer { +public class CsTokenizer extends AntlrTokenizer { private boolean ignoreUsings = false; @@ -28,283 +27,128 @@ public class CsTokenizer implements Tokenizer { } } - @Override - public void tokenize(SourceCode sourceCode, Tokens tokenEntries) { - try (Tokenizer tokenizer = new Tokenizer(sourceCode.getCodeBuffer().toString())) { - Token token = tokenizer.getNextToken(); - - while (!token.equals(Token.EOF)) { - Token lookAhead = tokenizer.getNextToken(); - - // Ignore using directives - // Only using directives should be ignored, because these are used - // to import namespaces - // - // Using directive: 'using System.Math;' - // Using statement: 'using (Font font1 = new Font(..)) { .. }' - if (ignoreUsings && "using".equals(token.image) && !"(".equals(lookAhead.image)) { - // We replace the 'using' token by a random token, because it - // should not be part of - // any duplication block. When we omit it from the token stream, - // there is a change that - // we get a duplication block that starts before the 'using' - // directives and ends afterwards. - String randomTokenText = RandomStringUtils.randomAlphanumeric(20); - - token = new Token(randomTokenText, token.lineNumber); - // Skip all other tokens of the using directive to prevent a - // partial matching - while (!";".equals(lookAhead.image) && !lookAhead.equals(Token.EOF)) { - lookAhead = tokenizer.getNextToken(); - } - } - if (!";".equals(token.image)) { - tokenEntries.add(new TokenEntry(token.image, sourceCode.getFileName(), token.lineNumber)); - } - token = lookAhead; - } - tokenEntries.add(TokenEntry.getEOF()); - } catch (IOException e) { - e.printStackTrace(); - } - } - public void setIgnoreUsings(boolean ignoreUsings) { this.ignoreUsings = ignoreUsings; } - private static class Tokenizer implements Closeable { - private boolean endOfFile; - private int line; - private final PushbackReader reader; + @Override + protected AntlrTokenManager getLexerForSource(final SourceCode sourceCode) { + final CharStream charStream = AntlrTokenizer.getCharStreamFromSourceCode(sourceCode); + return new AntlrTokenManager(new CSharpLexer(charStream), sourceCode.getFileName()); + } - Tokenizer(String sourceCode) { - endOfFile = false; - line = 1; - reader = new PushbackReader(new BufferedReader(new CharArrayReader(sourceCode.toCharArray()))); + @Override + protected AntlrTokenFilter getTokenFilter(final AntlrTokenManager tokenManager) { + return new CsTokenFilter(tokenManager, ignoreUsings); + } + + /** + * The {@link CsTokenFilter} extends the {@link AntlrTokenFilter} to discard + * C#-specific tokens. + *

+ * By default, it enables annotation-based CPD suppression. + * If the --ignoreUsings flag is provided, using directives are filtered out. + *

+ */ + private static class CsTokenFilter extends AntlrTokenFilter { + private enum UsingState { + KEYWORD, // just encountered the using keyword + IDENTIFIER, // just encountered an identifier or var keyword } - public Token getNextToken() { - if (endOfFile) { - return Token.EOF; - } + private final boolean ignoreUsings; + private boolean discardingUsings = false; + private boolean discardingNL = false; - try { - int ic = reader.read(); - char c; - StringBuilder b; - while (ic != -1) { - c = (char) ic; - switch (c) { - // new line - case '\n': - line++; - ic = reader.read(); - break; - - // white space - case ' ': - case '\t': - case '\r': - ic = reader.read(); - break; - - case ';': - return new Token(";", line); - - // < << <= <<= > >> >= >>= - case '<': - case '>': - ic = reader.read(); - if (ic == '=') { - return new Token(c + "=", line); - } else if (ic == c) { - ic = reader.read(); - if (ic == '=') { - return new Token(c + c + "=", line); - } else { - reader.unread(ic); - return new Token(String.valueOf(c) + c, line); - } - } else { - reader.unread(ic); - return new Token(String.valueOf(c), line); - } - - // = == & &= && | |= || + += ++ - -= -- - case '=': - case '&': - case '|': - case '+': - case '-': - ic = reader.read(); - if (ic == '=' || ic == c) { - return new Token(c + String.valueOf((char) ic), line); - } else { - reader.unread(ic); - return new Token(String.valueOf(c), line); - } - - // ! != * *= % %= ^ ^= ~ ~= - case '!': - case '*': - case '%': - case '^': - case '~': - ic = reader.read(); - if (ic == '=') { - return new Token(c + "=", line); - } else { - reader.unread(ic); - return new Token(String.valueOf(c), line); - } - - // strings & chars - case '"': - case '\'': - int beginLine = line; - b = new StringBuilder(); - b.append(c); - while ((ic = reader.read()) != c) { - if (ic == -1) { - break; - } - b.append((char) ic); - if (ic == '\\') { - int next = reader.read(); - if (next != -1) { - b.append((char) next); - - if (next == '\n') { - line++; - } - } - } else if (ic == '\n') { - line++; - } - } - if (ic != -1) { - b.append((char) ic); - } - return new Token(b.toString(), beginLine); - - // / /= /*...*/ //... - case '/': - ic = reader.read(); - c = (char) ic; - switch (c) { - case '*': - // int beginLine = line; - int state = 1; - b = new StringBuilder(); - b.append("/*"); - - while ((ic = reader.read()) != -1) { - c = (char) ic; - b.append(c); - - if (c == '\n') { - line++; - } - - if (state == 1) { - if (c == '*') { - state = 2; - } - } else { - if (c == '/') { - ic = reader.read(); - break; - } else if (c != '*') { - state = 1; - } - } - } - // ignore the /* comment - // tokenEntries.add(new TokenEntry(b.toString(), - // sourceCode.getFileName(), beginLine)); - break; - - case '/': - b = new StringBuilder(); - b.append("//"); - while ((ic = reader.read()) != '\n') { - if (ic == -1) { - break; - } - b.append((char) ic); - } - // ignore the // comment - // tokenEntries.add(new TokenEntry(b.toString(), - // sourceCode.getFileName(), line)); - break; - - case '=': - return new Token("/=", line); - - default: - reader.unread(ic); - return new Token("/", line); - } - break; - - default: - // [a-zA-Z_][a-zA-Z_0-9]* - if (Character.isJavaIdentifierStart(c)) { - b = new StringBuilder(); - do { - b.append(c); - ic = reader.read(); - c = (char) ic; - } while (Character.isJavaIdentifierPart(c)); - reader.unread(ic); - return new Token(b.toString(), line); - } else if (Character.isDigit(c) || c == '.') { - // numbers - b = new StringBuilder(); - do { - b.append(c); - if (c == 'e' || c == 'E') { - ic = reader.read(); - c = (char) ic; - if ("1234567890-".indexOf(c) == -1) { - break; - } - b.append(c); - } - ic = reader.read(); - c = (char) ic; - } while ("1234567890.iIlLfFdDsSuUeExX".indexOf(c) != -1); - reader.unread(ic); - return new Token(b.toString(), line); - } else { - // anything else - return new Token(String.valueOf(c), line); - } - } - } - } catch (IOException e) { - e.printStackTrace(); - } - endOfFile = true; - return Token.EOF; + CsTokenFilter(final AntlrTokenManager tokenManager, boolean ignoreUsings) { + super(tokenManager); + this.ignoreUsings = ignoreUsings; } @Override - public void close() throws IOException { - reader.close(); + protected void analyzeToken(final AntlrToken currentToken) { + skipNewLines(currentToken); } - } - private static class Token { - public static final Token EOF = new Token("EOF", -1); + @Override + protected void analyzeTokens(final AntlrToken currentToken, final Iterable remainingTokens) { + skipUsingDirectives(currentToken, remainingTokens); + } - public final String image; - public final int lineNumber; + private void skipUsingDirectives(final AntlrToken currentToken, final Iterable remainingTokens) { + final int type = currentToken.getType(); + if (type == CSharpLexer.USING && isUsingDirective(remainingTokens)) { + discardingUsings = true; + } else if (type == CSharpLexer.SEMICOLON) { + discardingUsings = false; + } + } - Token(String image, int lineNumber) { - this.image = image; - this.lineNumber = lineNumber; + private boolean isUsingDirective(final Iterable remainingTokens) { + UsingState usingState = UsingState.KEYWORD; + for (final AntlrToken token : remainingTokens) { + final int type = token.getType(); + if (usingState == UsingState.KEYWORD) { + // The previous token was a using keyword. + switch (type) { + case CSharpLexer.STATIC: + // Definitely a using directive. + // Example: using static System.Math; + return true; + case CSharpLexer.VAR: + // Definitely a using statement. + // Example: using var font1 = new Font("Arial", 10.0f); + return false; + case CSharpLexer.OPEN_PARENS: + // Definitely a using statement. + // Example: using (var font1 = new Font("Arial", 10.0f); + return false; + case CSharpLexer.IDENTIFIER: + // This is either a type for a using statement or an alias for a using directive. + // Example (directive): using Project = PC.MyCompany.Project; + // Example (statement): using Font font1 = new Font("Arial", 10.0f); + usingState = UsingState.IDENTIFIER; + break; + default: + // Some unknown construct? + return false; + } + } else if (usingState == UsingState.IDENTIFIER) { + // The previous token was an identifier. + switch (type) { + case CSharpLexer.ASSIGNMENT: + // Definitely a using directive. + // Example: using Project = PC.MyCompany.Project; + return true; + case CSharpLexer.IDENTIFIER: + // Definitely a using statement. + // Example: using Font font1 = new Font("Arial", 10.0f); + return false; + case CSharpLexer.DOT: + // This should be considered part of the same type; revert to previous state. + // Example (directive): using System.Text; + // Example (statement): using System.Drawing.Font font1 = new Font("Arial", 10.0f); + usingState = UsingState.KEYWORD; + break; + case CSharpLexer.SEMICOLON: + // End of using directive. + return true; + default: + // Some unknown construct? + return false; + } + } + } + return false; + } + + private void skipNewLines(final AntlrToken currentToken) { + discardingNL = currentToken.getType() == CSharpLexer.NL; + } + + @Override + protected boolean isLanguageSpecificDiscarding() { + return discardingUsings || discardingNL; } } } diff --git a/pmd-cs/src/test/java/net/sourceforge/pmd/cpd/CsTokenizerTest.java b/pmd-cs/src/test/java/net/sourceforge/pmd/cpd/CsTokenizerTest.java index 20aefc8194..ed87562537 100644 --- a/pmd-cs/src/test/java/net/sourceforge/pmd/cpd/CsTokenizerTest.java +++ b/pmd-cs/src/test/java/net/sourceforge/pmd/cpd/CsTokenizerTest.java @@ -12,6 +12,8 @@ import java.util.List; import org.junit.Before; import org.junit.Test; +import net.sourceforge.pmd.lang.ast.TokenMgrError; + public class CsTokenizerTest { private CsTokenizer tokenizer; @@ -46,7 +48,7 @@ public class CsTokenizerTest { public void testSimpleClassMethodMultipleLines() { tokenizer.tokenize(toSourceCode("class Foo {\n" + " public String foo(int a) {\n" + " int i = a;\n" + " return \"x\" + a;\n" + " }\n" + "}"), tokens); - assertEquals(22, tokens.size()); + assertEquals(24, tokens.size()); List tokenList = tokens.getTokens(); assertEquals(1, tokenList.get(0).getBeginLine()); assertEquals(2, tokenList.get(4).getBeginLine()); @@ -56,13 +58,12 @@ public class CsTokenizerTest { @Test public void testStrings() { tokenizer.tokenize(toSourceCode("String s =\"aaa \\\"b\\n\";"), tokens); - assertEquals(5, tokens.size()); + assertEquals(6, tokens.size()); } - @Test + @Test(expected = TokenMgrError.class) public void testOpenString() { tokenizer.tokenize(toSourceCode("String s =\"aaa \\\"b\\"), tokens); - assertEquals(5, tokens.size()); } @Test @@ -91,7 +92,7 @@ public class CsTokenizerTest { + " a++; \n" + " a /= 3e2; \n" + " float f = -3.1; \n" + " f *= 2; \n" + " bool b = ! (f == 2.0 || f >= 1.0 && f <= 2.0) \n" + " }\n" + "}"), tokens); - assertEquals(50, tokens.size()); + assertEquals(57, tokens.size()); } @Test @@ -119,8 +120,9 @@ public class CsTokenizerTest { public void testIgnoreUsingDirectives() { tokenizer.setIgnoreUsings(true); tokenizer.tokenize(toSourceCode("using System.Text;\n"), tokens); + assertEquals(1, tokens.size()); assertNotEquals("using", tokens.getTokens().get(0).toString()); - assertEquals(2, tokens.size()); + assertEquals(TokenEntry.EOF, tokens.getTokens().get(0)); } @Test @@ -132,6 +134,15 @@ public class CsTokenizerTest { assertEquals("using", tokens.getTokens().get(0).toString()); } + @Test + public void testUsingVarStatementsAreNotIgnored() { + tokenizer.setIgnoreUsings(true); + tokenizer.tokenize(toSourceCode( + "using var font1 = new Font(\"Arial\", 10.0f);\n" + " byte charset = font1.GdiCharSet;\n"), + tokens); + assertEquals("using", tokens.getTokens().get(0).toString()); + } + private SourceCode toSourceCode(String source) { return new SourceCode(new SourceCode.StringCodeLoader(source)); } diff --git a/pmd-java/etc/grammar/Java.jjt b/pmd-java/etc/grammar/Java.jjt index ffa88e5f4c..9a24e34d5e 100644 --- a/pmd-java/etc/grammar/Java.jjt +++ b/pmd-java/etc/grammar/Java.jjt @@ -1,30 +1,14 @@ /** - * Make Resources #void, rename ResourceSpecification to ResourceList, - * put a LocalVariableDeclaration node inside Resource. - * Merge different increment/decrement expressions. - * Clément Fournier 07/2019 + * Add support for record types introduced as a preview language + * feature with Java 14. See JEP 359. + * Andreas Dangel 02/2020 *==================================================================== - * Nest annotations inside the node they apply to. The node TypeBound is - * removed. Allow type annotations on InstanceofExpression, MethodReference - * and bounds of an IntersectionType. - * Clément Fournier 06/2019 + * Add support for pattern matching for instance of introduced + * as a preview language feature with Java 14. See JEP 305. + * Clément Fournier 02/2020 *==================================================================== - * Change expression, type and annotation grammar to remove unnecessary nodes, - * eliminate some inconsistencies, and most importantly have an expressive tree - * for primary expressions. Expressions and types now appear to be left-recursive. - * This also introduces AmbiguousName, which are pushed only in syntactically - * ambiguous contexts: https://docs.oracle.com/javase/specs/jls/se9/html/jls-6.html#jls-6.5.1 - * - * Those are the first grammar changes for 7.0.0. - * Refs: - * #1661 [java] About operator nodes - * #1367 [java] Parsing error on annotated subclass - * #1150 [java] ClassOrInterfaceType AST improvements - * #1019 [java] Breaking Java Grammar changes for PMD 7.0.0 - * #997 [java] Java8 parsing corner case with annotated array types - * #910 [java] AST inconsistency between primitive and reference type arrays - * #497 [java] RFC: new layer of abstraction atop PrimaryExpressions - * Clément Fournier 04/2019 + * Switch Expressions are now a standard feature of Java 14. + * Andreas Dangel 02/2020 *==================================================================== * Add support for the yield statement introduced as a preview language * feature with Java 13. See JEP 354. @@ -230,7 +214,6 @@ options { UNICODE_INPUT=true; CACHE_TOKENS = true; STATIC = false; - MULTI = true; VISITOR = true; NODE_PACKAGE="net.sourceforge.pmd.lang.java.ast"; @@ -561,12 +544,12 @@ TOKEN : | < TEXT_BLOCK_LITERAL: "\"\"\"" ()* - ( ~["\"", "\\"] | "\"" ~["\""] | "\"\"" ~["\""] | )* + ( ~["\"", "\\"] | "\"" ~["\""] | "\"\"" ~["\""] | | "\\" )* "\"\"\"" > | < #STRING_ESCAPE: "\\" - ( ["n","t","b","r","f","\\","'","\""] + ( ["n","t","b","r","f","s","\\","'","\""] // octal escapes | ["0"-"7"] ( ["0"-"7"] )? | ["0"-"3"] ["0"-"7"] ["0"-"7"] @@ -861,10 +844,10 @@ void TypeDeclaration() #void: ModifierList() ( ClassOrInterfaceDeclaration() - | - LOOKAHEAD({isKeyword("enum")}) EnumDeclaration() - | - AnnotationTypeDeclaration() + | AnnotationTypeDeclaration() + | LOOKAHEAD({isKeyword("enum")}) EnumDeclaration() + | LOOKAHEAD({isKeyword("record")}) RecordDeclaration() + ) } @@ -924,6 +907,67 @@ void EnumConstant(): ModAnnotationList() VariableDeclaratorId() [ ArgumentList() ] [ AnonymousClassDeclaration() ] } +void RecordDeclaration(): +{ + JavaccToken t; +} +{ + t = { + if (!"record".equals(t.image)) { + throw new ParseException("ERROR: expecting record"); + } + } + t= {jjtThis.setImage(t.image);} + [ TypeParameters() ] + RecordComponentList() + [ ImplementsList() ] + RecordBody() +} + +void RecordComponentList() : +{} +{ + "(" [ RecordComponent() ("," RecordComponent())* ] ")" +} + +void RecordComponent(): +{} +{ + ModAnnotationList() + FormalParamType() + VariableIdWithDims() +} + +void RecordBody(): +{} +{ + "{" + ( RecordBodyDeclaration() )* + "}" +} + +void RecordBodyDeclaration() #void : +{} +{ + LOOKAHEAD(RecordCtorLookahead()) ModifierList() RecordConstructorDeclaration() + | + ClassOrInterfaceBodyDeclaration() +} + +private void RecordCtorLookahead() #void: +{} +{ + ModifierList() [ TypeParameters() ] ("throws" | "{") +} + +void RecordConstructorDeclaration(): +{} +{ + [TypeParameters()] + { jjtThis.setImage(token.image); } + Block() +} + void TypeParameters(): {} { @@ -949,6 +993,7 @@ void ClassOrInterfaceBodyDeclaration() #void: | ModifierList() ( LOOKAHEAD(3) ClassOrInterfaceDeclaration() | LOOKAHEAD({isKeyword("enum")}) EnumDeclaration() + | LOOKAHEAD({isKeyword("record")}) RecordDeclaration() | LOOKAHEAD( [ TypeParameters() ] "(" ) ConstructorDeclaration() | LOOKAHEAD( Type() (AnnotationList() "[" "]")* ( "," | "=" | ";" ) ) FieldDeclaration() | LOOKAHEAD(2) MethodDeclaration() @@ -1455,8 +1500,18 @@ void InstanceOfExpression() #void: RelationalExpression() [ LOOKAHEAD(1) ("instanceof" - AnnotatedRefType() #TypeExpression - {jjtThis.setOp(BinaryOp.INSTANCEOF);} + AnnotatedRefType() [ VariableDeclaratorId() #TypeTestPattern(2) ] + { + jjtThis.setOp(BinaryOp.INSTANCEOF); + AbstractJavaNode top = jjtree.popNode(); + if (top instanceof ASTPattern) { + top = new ASTPatternExpression((ASTPattern) top); + } else { + top = new ASTTypeExpression((ASTType) top); + } + jjtree.pushNode(top); + } + {} ) #InfixExpression(2) ] } @@ -2162,17 +2217,13 @@ void ForUpdate() : void BreakStatement() : {} { - "break" - [ LOOKAHEAD( ";") { setLastTokenImage(jjtThis); } - | Expression() - ] - ";" + "break" [ { setLastTokenImage(jjtThis); } ] ";" } void ContinueStatement() : {} { - "continue" [ { setLastTokenImage(jjtThis); } ] ";" + "continue" [ { setLastTokenImage(jjtThis); } ] ";" } void ReturnStatement() : @@ -2465,8 +2516,7 @@ String VoidName() #void: // normally by jjtree, but rather by the disambiguation // hooks spread across the parser //noinspection UnusedProduction -void VariableAccess(): -{} -{ - -} +void VariableAccess(): {} { } +// those are created manually +void TypeExpression(): {} { } +void PatternExpression(): {} { } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/JavaLanguageModule.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/JavaLanguageModule.java index e0827c2cc6..04858ee65e 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/JavaLanguageModule.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/JavaLanguageModule.java @@ -28,9 +28,10 @@ public class JavaLanguageModule extends BaseLanguageModule { addVersions(new JavaLanguageHandler(10), false, "10", "1.10"); addVersion("11", new JavaLanguageHandler(11), false); addVersion("12", new JavaLanguageHandler(12), false); - addVersion("12-preview", new JavaLanguageHandler(12, true), false); - addVersion("13", new JavaLanguageHandler(13), true); + addVersion("13", new JavaLanguageHandler(13), false); addVersion("13-preview", new JavaLanguageHandler(13, true), false); + addVersion("14", new JavaLanguageHandler(14), true); // 14 is the default + addVersion("14-preview", new JavaLanguageHandler(14, true), false); } } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTAnyTypeDeclaration.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTAnyTypeDeclaration.java index 78a8c563ed..2ec7989a9a 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTAnyTypeDeclaration.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTAnyTypeDeclaration.java @@ -19,7 +19,7 @@ import net.sourceforge.pmd.lang.java.symbols.JClassSymbol; /** - * Groups enum, class, annotation and interface declarations under a common + * Groups class, enum, record, annotation and interface declarations under a common * supertype. */ public interface ASTAnyTypeDeclaration diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTInstanceOfExpression.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTInstanceOfExpression.java index 882a16aaf8..e1744cb85a 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTInstanceOfExpression.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTInstanceOfExpression.java @@ -9,7 +9,7 @@ package net.sourceforge.pmd.lang.java.ast; * *
  *
- * InstanceOfExpression ::= {@linkplain ASTExpression Expression} "instanceof" {@linkplain ASTTypeExpression TypeExpression}
+ * InstanceOfExpression ::= {@linkplain ASTExpression Expression} "instanceof" ({@linkplain ASTTypeExpression TypeExpression} | {@link ASTPattern Pattern})
  *
  * 
* diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTModifierList.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTModifierList.java index 5b2f272b9a..d57dc40bee 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTModifierList.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTModifierList.java @@ -223,6 +223,12 @@ public final class ASTModifierList extends AbstractJavaNode { effective.add(FINAL); } + @Override + public void visit(ASTRecordComponent node, Set effective) { + effective.add(PUBLIC); + effective.add(FINAL); + } + @Override public void visit(ASTAnonymousClassDeclaration node, Set effective) { ASTBodyDeclaration enclosing = node.ancestors(ASTBodyDeclaration.class).first(); diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTPattern.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTPattern.java new file mode 100644 index 0000000000..7df29d5c58 --- /dev/null +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTPattern.java @@ -0,0 +1,29 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.java.ast; + +import net.sourceforge.pmd.annotation.Experimental; + +/** + * A pattern (for pattern matching constructs like {@link ASTInstanceOfExpression InstanceOfExpression}). + * This is a JDK 14 preview feature and is subject to change. + * + *

This interface will be implemented by all forms of patterns. For + * now, only type test patterns are supported. Record deconstruction + * patterns are in the works for JDK 15 preview. + * + *

See https://openjdk.java.net/jeps/305, https://openjdk.java.net/jeps/8235186 + * + *

+ *
+ * Pattern ::= {@link ASTTypeTestPattern TypeTestPattern}
+ *
+ * 
+ */ +@Experimental +public interface ASTPattern extends JavaNode { + + +} diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTPatternExpression.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTPatternExpression.java new file mode 100644 index 0000000000..183e16b27f --- /dev/null +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTPatternExpression.java @@ -0,0 +1,74 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.java.ast; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import net.sourceforge.pmd.lang.java.ast.InternalInterfaces.AtLeastOneChild; +import net.sourceforge.pmd.lang.java.typeresolution.typedefinition.JavaTypeDefinition; + +/** + * Wraps a {@link ASTPattern} node but presents the interface of {@link ASTExpression}. + * This is only used in the following contexts: + *
    + *
  • As the right-hand side of {@link BinaryOp#INSTANCEOF instanceof expressions}. + *
+ * + *
+ *
+ * PatternExpression ::= {@link ASTPattern Pattern}
+ *
+ * 
+ */ +public final class ASTPatternExpression extends AbstractJavaNode implements ASTPrimaryExpression, AtLeastOneChild, LeftRecursiveNode { + + ASTPatternExpression(int id) { + super(id); + } + + ASTPatternExpression(ASTPattern wrapped) { + this(JavaParserImplTreeConstants.JJTPATTERNEXPRESSION); + this.jjtAddChild(wrapped, 0); + copyTextCoordinates((AbstractJavaNode) wrapped); + } + + @Override + public Object jjtAccept(JavaParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } + + + @Override + public void jjtAccept(SideEffectingVisitor visitor, T data) { + visitor.visit(this, data); + } + + /** Gets the wrapped type node. */ + public ASTPattern getPattern() { + return (ASTPattern) getChild(0); + } + + + /** Returns 0, patterns can never be parenthesized. */ + @Override + public int getParenthesisDepth() { + return 0; + } + + /** Returns false, patterns can never be parenthesized. */ + @Override + public boolean isParenthesized() { + return false; + } + + @Override + public @Nullable JavaTypeDefinition getTypeDefinition() { + ASTPattern pattern = getPattern(); + if (pattern instanceof ASTTypeTestPattern) { + return ((ASTTypeTestPattern) pattern).getTypeNode().getTypeDefinition(); + } + return null; + } +} diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordBody.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordBody.java new file mode 100644 index 0000000000..ef9646dfcd --- /dev/null +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordBody.java @@ -0,0 +1,37 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + + +package net.sourceforge.pmd.lang.java.ast; + +import net.sourceforge.pmd.annotation.Experimental; + +/** + * Defines the body of a {@linkplain ASTRecordDeclaration RecordDeclaration} (JDK 14 preview feature). + * This can contain additional methods and or constructors. + * + *
+ *
+ * RecordBody ::= "{" (   {@linkplain ASTRecordConstructorDeclaration RecordConstructorDeclaration}
+ *                      | {@linkplain ASTClassOrInterfaceBodyDeclaration ClassOrInterfaceBodyDeclaration} )* "}"
+ *
+ * 
+ * + */ +@Experimental +public final class ASTRecordBody extends AbstractJavaNode implements ASTTypeBody { + ASTRecordBody(int id) { + super(id); + } + + @Override + public Object jjtAccept(JavaParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } + + @Override + public void jjtAccept(SideEffectingVisitor visitor, T data) { + visitor.visit(this, data); + } +} diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordComponent.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordComponent.java new file mode 100644 index 0000000000..6716115714 --- /dev/null +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordComponent.java @@ -0,0 +1,50 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + + +package net.sourceforge.pmd.lang.java.ast; + +import net.sourceforge.pmd.annotation.Experimental; + +/** + * Defines a single component of a {@linkplain ASTRecordDeclaration RecordDeclaration} (JDK 14 preview feature). + * + *
+ *
+ * RecordComponent ::= ({@linkplain ASTAnnotation Annotation})*
+ *                     {@linkplain ASTType Type}
+ *                     ( "..." )?
+ *                     {@linkplain ASTVariableDeclaratorId VariableDeclaratorId}
+ *
+ * 
+ */ +@Experimental +public final class ASTRecordComponent extends AbstractJavaNode implements AccessNode { + + ASTRecordComponent(int id) { + super(id); + } + + @Override + public Object jjtAccept(JavaParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } + + @Override + public void jjtAccept(SideEffectingVisitor visitor, T data) { + visitor.visit(this, data); + } + + public boolean isVarargs() { + return getTypeNode() instanceof ASTArrayType && ((ASTArrayType) getTypeNode()).getDimensions().getLastChild().isVarargs(); + } + + public ASTType getTypeNode() { + return getFirstChildOfType(ASTType.class); + } + + public ASTVariableDeclaratorId getVarId() { + return getFirstChildOfType(ASTVariableDeclaratorId.class); + } +} diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordComponentList.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordComponentList.java new file mode 100644 index 0000000000..1ed7f3d71a --- /dev/null +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordComponentList.java @@ -0,0 +1,36 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + + +package net.sourceforge.pmd.lang.java.ast; + +import net.sourceforge.pmd.annotation.Experimental; +import net.sourceforge.pmd.lang.java.ast.ASTList.ASTNonEmptyList; + +/** + * Defines the state description of a {@linkplain ASTRecordDeclaration RecordDeclaration} (JDK 14 preview feature). + * + *
+ *
+ * RecordComponentList ::= "(" ( {@linkplain ASTRecordComponent RecordComponent} ( "," {@linkplain ASTRecordComponent RecordComponent} )* )? ")"
+ *
+ * 
+ */ +@Experimental +public final class ASTRecordComponentList extends ASTNonEmptyList { + + ASTRecordComponentList(int id) { + super(id, ASTRecordComponent.class); + } + + @Override + public Object jjtAccept(JavaParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } + + @Override + public void jjtAccept(SideEffectingVisitor visitor, T data) { + visitor.visit(this, data); + } +} diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordConstructorDeclaration.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordConstructorDeclaration.java new file mode 100644 index 0000000000..aafe6e1b2a --- /dev/null +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordConstructorDeclaration.java @@ -0,0 +1,50 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + + +package net.sourceforge.pmd.lang.java.ast; + +import net.sourceforge.pmd.annotation.Experimental; + +/** + * This defines a compact constructor for a {@link ASTRecordDeclaration RecordDeclaration} + * (JDK 14 preview feature). + * + * TODO make implicit formal parameter node and implement ASTMethodOrConstructorDeclaration. + * + *
+ *
+ * RecordConstructorDeclaration ::=  {@link ASTModifierList Modifiers}
+ *                                   {@link ASTTypeParameters TypeParameters}?
+ *                                   <IDENTIFIER>
+ *                                   {@link ASTBlock Block}
+ *
+ * 
+ */ +@Experimental +public final class ASTRecordConstructorDeclaration extends AbstractJavaNode implements ASTAnyTypeBodyDeclaration { + + ASTRecordConstructorDeclaration(int id) { + super(id); + } + + @Override + public Object jjtAccept(JavaParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } + + @Override + public void jjtAccept(SideEffectingVisitor visitor, T data) { + visitor.visit(this, data); + } + + @Override + public ASTRecordConstructorDeclaration getDeclarationNode() { + return this; + } + + public ASTBlock getBody() { + return getFirstChildOfType(ASTBlock.class); + } +} diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordDeclaration.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordDeclaration.java new file mode 100644 index 0000000000..80fa12a7a8 --- /dev/null +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordDeclaration.java @@ -0,0 +1,49 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + + +package net.sourceforge.pmd.lang.java.ast; + +import net.sourceforge.pmd.annotation.Experimental; +import net.sourceforge.pmd.lang.ast.Node; + +/** + * A record declaration is a special data class type (JDK 14 preview feature). + * This is a {@linkplain Node#isFindBoundary() find boundary} for tree traversal methods. + * + *
+ *
+ * RecordDeclaration ::= {@link ASTModifierList ModifierList}
+ *                       "record"
+ *                       <IDENTIFIER>
+ *                       {@linkplain ASTTypeParameters TypeParameters}?
+ *                       {@linkplain ASTRecordComponentList RecordComponents}
+ *                       {@linkplain ASTImplementsList ImplementsList}?
+ *                       {@linkplain ASTRecordBody RecordBody}
+ *
+ * 
+ * + * @see
JEP 359: Records (Preview) + */ +@Experimental +public final class ASTRecordDeclaration extends AbstractAnyTypeDeclaration { + ASTRecordDeclaration(int id) { + super(id); + } + + @Override + public Object jjtAccept(JavaParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } + + @Override + public void jjtAccept(SideEffectingVisitor visitor, T data) { + visitor.visit(this, data); + } + + /** Returns the list of record components. */ + public ASTRecordComponentList getComponentList() { + return getFirstChildOfType(ASTRecordComponentList.class); + } +} diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTStringLiteral.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTStringLiteral.java index ca6e43d4ae..f2386ceff2 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTStringLiteral.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTStringLiteral.java @@ -4,7 +4,11 @@ package net.sourceforge.pmd.lang.java.ast; +import java.util.Arrays; +import java.util.List; + import org.apache.commons.lang3.StringEscapeUtils; +import org.apache.commons.lang3.StringUtils; /** * Represents a string literal. The image of this node is the literal as it appeared @@ -13,6 +17,8 @@ import org.apache.commons.lang3.StringEscapeUtils; */ public final class ASTStringLiteral extends AbstractLiteral implements ASTLiteral { + private static final String TEXTBLOCK_DELIMITER = "\"\"\""; + private boolean isTextBlock; ASTStringLiteral(int id) { @@ -49,10 +55,135 @@ public final class ASTStringLiteral extends AbstractLiteral implements ASTLitera /** Returns the value without delimiters and unescaped. */ @Override public String getConstValue() { - // FIXME support text blocks - CharSequence image = getText(); - CharSequence woDelims = image.subSequence(1, image.length() - 1); - return StringEscapeUtils.UNESCAPE_JAVA.translate(woDelims); + if (isTextBlock()) { + return determineTextBlockContent(getImage()); + } else { + CharSequence image = getText(); + CharSequence woDelims = image.subSequence(1, image.length() - 1); + return StringEscapeUtils.UNESCAPE_JAVA.translate(woDelims); + } } + static String determineTextBlockContent(String image) { + // normalize line endings to LF + String content = image.replaceAll("\r\n|\r", "\n"); + int start = determineContentStart(content); + content = content.substring(start, content.length() - TEXTBLOCK_DELIMITER.length()); + + int prefixLength = Integer.MAX_VALUE; + List lines = Arrays.asList(content.split("\\n")); + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + // compute common prefix + if (!StringUtils.isAllBlank(line) || i == lines.size() - 1) { + prefixLength = Math.min(prefixLength, countLeadingWhitespace(line)); + } + } + if (prefixLength == Integer.MAX_VALUE) { + // common prefix not found + prefixLength = 0; + } + StringBuilder sb = new StringBuilder(content.length()); + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + // remove common whitespace prefix + if (!StringUtils.isAllBlank(line) && line.length() >= prefixLength) { + line = line.substring(prefixLength); + } + line = removeTrailingWhitespace(line); + sb.append(line); + + boolean isLastLine = i == lines.size() - 1; + boolean isFirstLine = i == 0; + if (!isLastLine || !isFirstLine && !StringUtils.isAllBlank(line)) { + sb.append('\n'); + } + } + + interpretEscapeSequences(sb); + return sb.toString(); + } + + private static void interpretEscapeSequences(StringBuilder sb) { + // interpret escape sequences "\" (line continuation), "n","t","b","r","f", "s", "\"", "\'", "\\" + // we need to interpret everything in one pass, so regex replacement is inappropriate + for (int i = 0; i < sb.length(); i++) { + char c = sb.charAt(i); + if (c == '\\' && i < sb.length() - 1) { + char cnext = sb.charAt(i + 1); + switch (cnext) { + case '\n': + // line continuation + sb.delete(i, i + 2); + break; + case '\\': + sb.deleteCharAt(i); + break; + case 'n': + sb.deleteCharAt(i); + sb.setCharAt(i, '\n'); + break; + case 't': + sb.deleteCharAt(i); + sb.setCharAt(i, '\t'); + break; + case 'b': + sb.deleteCharAt(i); + sb.setCharAt(i, '\b'); + break; + case 'r': + sb.deleteCharAt(i); + sb.setCharAt(i, '\r'); + break; + case 'f': + sb.deleteCharAt(i); + sb.setCharAt(i, '\f'); + break; + case 's': + sb.deleteCharAt(i); + sb.setCharAt(i, ' '); + break; + case '"': + sb.deleteCharAt(i); + sb.setCharAt(i, '"'); + break; + case '\'': + sb.deleteCharAt(i); + sb.setCharAt(i, '\''); + break; + default: + // unknown escape - do nothing - it stays + } + } + } + } + + private static int determineContentStart(String s) { + int start = TEXTBLOCK_DELIMITER.length(); // this is the opening delimiter + // the content begins after at the first character after the line terminator + // of the opening delimiter + while (start < s.length() && Character.isWhitespace(s.charAt(start))) { + if (s.charAt(start) == '\n') { + return start + 1; + } + start++; + } + return start; + } + + private static int countLeadingWhitespace(String s) { + int count = 0; + while (count < s.length() && Character.isWhitespace(s.charAt(count))) { + count++; + } + return count; + } + + private static String removeTrailingWhitespace(String s) { + int endIndexIncluding = s.length() - 1; + while (endIndexIncluding >= 0 && Character.isWhitespace(s.charAt(endIndexIncluding))) { + endIndexIncluding--; + } + return s.substring(0, endIndexIncluding + 1); + } } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTTypeBody.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTTypeBody.java index f0b4322841..36aff6f930 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTTypeBody.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTTypeBody.java @@ -13,11 +13,10 @@ import net.sourceforge.pmd.lang.ast.NodeStream; * * TypeBody ::= {@link ASTClassOrInterfaceBody ClassOrInterfaceBody} * | {@link ASTEnumBody EnumBody} - * | {@link ASTAnnotationTypeBody} + * | {@link ASTRecordBody RecordBody} + * | {@link ASTAnnotationTypeBody AnnotationTypeBody} * * - * - * @author Clément Fournier */ public interface ASTTypeBody extends JavaNode { diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTTypeTestPattern.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTTypeTestPattern.java new file mode 100644 index 0000000000..76a53e9b14 --- /dev/null +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTTypeTestPattern.java @@ -0,0 +1,51 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.java.ast; + +import net.sourceforge.pmd.annotation.Experimental; + +/** + * A type test pattern (JDK 14 preview feature). This can be found on + * the right-hand side of an {@link ASTInfixExpression InstanceOfExpression}, + * in a {@link ASTPatternExpression PatternExpression}. + * + *
+ *
+ * TypeTestPattern ::= {@linkplain ASTType Type} {@link ASTVariableDeclaratorId VariableDeclaratorId}
+ *
+ * 
+ */ +@Experimental +public final class ASTTypeTestPattern extends AbstractJavaNode implements ASTPattern { + + + ASTTypeTestPattern(int id) { + super(id); + } + + + @Override + public Object jjtAccept(JavaParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } + + @Override + public void jjtAccept(SideEffectingVisitor visitor, T data) { + visitor.visit(this, data); + } + + /** + * Gets the type against which the expression is tested. + */ + public ASTType getTypeNode() { + return (ASTType) getChild(0); + } + + /** Returns the declared variable. */ + public ASTVariableDeclaratorId getVarId() { + return (ASTVariableDeclaratorId) getChild(1); + } + +} diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTVariableDeclaratorId.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTVariableDeclaratorId.java index ee21f2067f..523406201c 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTVariableDeclaratorId.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTVariableDeclaratorId.java @@ -8,6 +8,7 @@ import java.util.List; import org.checkerframework.checker.nullness.qual.Nullable; +import net.sourceforge.pmd.annotation.Experimental; import net.sourceforge.pmd.annotation.InternalApi; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.java.symbols.JVariableSymbol; @@ -193,6 +194,16 @@ public final class ASTVariableDeclaratorId extends AbstractTypedSymbolDeclarator return getTypeNode() == null; } + /** + * Returns true if this is a binding variable in a + * {@linkplain ASTPattern pattern}. + */ + @Experimental + public boolean isPatternBinding() { + return getParent() instanceof ASTPattern; + } + + /** * Returns the initializer of the variable, or null if it doesn't exist. */ diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserVisitorAdapter.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserVisitorAdapter.java index 41d2f5ca9e..a2b20dfafa 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserVisitorAdapter.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserVisitorAdapter.java @@ -200,18 +200,21 @@ public class JavaParserVisitorAdapter implements JavaParserVisitor { return visit((ASTAnyTypeDeclaration) node, data); } - @Override public Object visit(ASTAnnotationTypeDeclaration node, Object data) { return visit((ASTAnyTypeDeclaration) node, data); } - @Override public Object visit(ASTEnumDeclaration node, Object data) { return visit((ASTAnyTypeDeclaration) node, data); } + @Override + public Object visit(ASTRecordDeclaration node, Object data) { + return visit((ASTAnyTypeDeclaration) node, data); + } + public Object visit(ASTAnyTypeDeclaration node, Object data) { return visit((JavaNode) node, data); diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserVisitorReducedAdapter.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserVisitorReducedAdapter.java index c8fcac5c59..ad0c06415e 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserVisitorReducedAdapter.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserVisitorReducedAdapter.java @@ -31,6 +31,13 @@ public class JavaParserVisitorReducedAdapter extends JavaParserVisitorAdapter { return visit((ASTAnyTypeDeclaration) node, data); } + + @Override + public Object visit(ASTRecordDeclaration node, Object data) { + return visit((ASTAnyTypeDeclaration) node, data); + } + + @Override public Object visit(ASTAnyTypeDeclaration node, Object data) { return visit((JavaNode) node, data); diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/internal/LanguageLevelChecker.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/internal/LanguageLevelChecker.java index d3c88ee800..d3e176727b 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/internal/LanguageLevelChecker.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/internal/LanguageLevelChecker.java @@ -6,8 +6,10 @@ package net.sourceforge.pmd.lang.java.ast.internal; import java.util.Locale; +import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; +import org.checkerframework.checker.nullness.qual.Nullable; import net.sourceforge.pmd.internal.util.IteratorUtil; import net.sourceforge.pmd.lang.ast.Node; @@ -30,6 +32,7 @@ import net.sourceforge.pmd.lang.java.ast.ASTMethodReference; import net.sourceforge.pmd.lang.java.ast.ASTModuleDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTNumericLiteral; import net.sourceforge.pmd.lang.java.ast.ASTReceiverParameter; +import net.sourceforge.pmd.lang.java.ast.ASTRecordDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTResource; import net.sourceforge.pmd.lang.java.ast.ASTStringLiteral; import net.sourceforge.pmd.lang.java.ast.ASTSwitchArrowBranch; @@ -39,6 +42,7 @@ import net.sourceforge.pmd.lang.java.ast.ASTTryStatement; import net.sourceforge.pmd.lang.java.ast.ASTType; import net.sourceforge.pmd.lang.java.ast.ASTTypeArguments; import net.sourceforge.pmd.lang.java.ast.ASTTypeParameters; +import net.sourceforge.pmd.lang.java.ast.ASTTypeTestPattern; import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId; import net.sourceforge.pmd.lang.java.ast.ASTYieldStatement; import net.sourceforge.pmd.lang.java.ast.JModifier; @@ -53,6 +57,8 @@ import net.sourceforge.pmd.lang.java.ast.SideEffectingVisitorAdapter; */ public class LanguageLevelChecker { + private static final Pattern SPACE_ESCAPE_PATTERN = Pattern.compile("(? { reportingStrategy.done(accumulator); } - private boolean check(Node node, LanguageFeature message, T acc) { - if (message.isAvailable(this.jdkVersion, this.preview)) { - return true; + private boolean check(Node node, LanguageFeature feature, T acc) { + String message = feature.errorMessage(this.jdkVersion, this.preview); + if (message != null) { + reportingStrategy.report(node, message, acc); + return false; } - - reportingStrategy.report(node, message.whenUnavailableMessage(), acc); - return false; + return true; } + private static String displayNameLower(String name) { + return name.replaceAll("__", "-") + .replace('_', ' ') + .toLowerCase(Locale.ROOT); + } + + private static String versionDisplayName(int jdk) { + if (jdk < 8) { + return "Java 1." + jdk; + } else { + return "Java " + jdk; + } + } + + + /** Those are just for the preview features. */ + private enum PreviewFeature implements LanguageFeature { + BREAK__WITH__VALUE_STATEMENTS(12, 12, false), + + COMPOSITE_CASE_LABEL(12, 13, true), + SWITCH_EXPRESSIONS(12, 13, true), + SWITCH_RULES(12, 13, true), + + TEXT_BLOCK_LITERALS(13, 14, false), + YIELD_STATEMENTS(13, 13, true), + + /** \s */ + SPACE_STRING_ESCAPES(14, 14, false), + RECORD_DECLARATIONS(14, 14, false), + TYPE_TEST_PATTERNS_IN_INSTANCEOF(14, 14, false); + + + private final int minPreviewVersion; + private final int maxPreviewVersion; + private final boolean wasStandardized; + + PreviewFeature(int minPreviewVersion, int maxPreviewVersion, boolean wasStandardized) { + this.minPreviewVersion = minPreviewVersion; + this.maxPreviewVersion = maxPreviewVersion; + this.wasStandardized = wasStandardized; + } + + + @Override + public String errorMessage(int jdk, boolean preview) { + boolean isStandard = wasStandardized && jdk > maxPreviewVersion; + boolean canBePreview = jdk >= minPreviewVersion && jdk <= maxPreviewVersion; + boolean isPreview = preview && canBePreview; + + if (isStandard || isPreview) { + return null; + } + + String message = StringUtils.capitalize(displayNameLower(name())); + if (canBePreview) { + message += " is a preview feature of JDK " + jdk; + } else if (wasStandardized) { + message = message + " was only standardized in Java " + (maxPreviewVersion + 1); + } else if (minPreviewVersion == maxPreviewVersion) { + message += " is a preview feature of JDK " + minPreviewVersion; + } else { + message += " is a preview feature of JDKs " + minPreviewVersion + " to " + maxPreviewVersion; + } + return message + ", you should select your language version accordingly"; + } + } + + /** Those use a max valid version. */ + private enum ReservedIdentifiers implements LanguageFeature { + ASSERT_AS_AN_IDENTIFIER(4, "assert"), + ENUM_AS_AN_IDENTIFIER(5, "enum"), + UNDERSCORE_AS_AN_IDENTIFIER(9, "_"), + VAR_AS_A_TYPE_NAME(10, "var"), + RECORD_AS_A_TYPE_NAME(14, "record"); + + private final int maxJdkVersion; + private final String reserved; + + ReservedIdentifiers(int minJdkVersion, String reserved) { + this.maxJdkVersion = minJdkVersion; + this.reserved = reserved; + } + + @Override + public String errorMessage(int jdk, boolean preview) { + if (jdk < this.maxJdkVersion) { + return null; + } + String s = displayNameLower(name()); + String usageType = s.substring(s.indexOf(' ') + 1); // eg "as an identifier" + return "Since " + LanguageLevelChecker.versionDisplayName(maxJdkVersion) + ", '" + reserved + "'" + + " is reserved and cannot be used " + usageType; + } + } + + /** Those use a min valid version. */ + private enum RegularLanguageFeature implements LanguageFeature { + + ASSERT_STATEMENTS(4), + + STATIC_IMPORT(5), + ENUMS(5), + GENERICS(5), + ANNOTATIONS(5), + FOREACH_LOOPS(5), + VARARGS_PARAMETERS(5), + HEXADECIMAL_FLOATING_POINT_LITERALS(5), + + UNDERSCORES_IN_NUMERIC_LITERALS(7), + BINARY_NUMERIC_LITERALS(7), + TRY_WITH_RESOURCES(7), + COMPOSITE_CATCH_CLAUSES(7), + DIAMOND_TYPE_ARGUMENTS(7), + + DEFAULT_METHODS(8), + RECEIVER_PARAMETERS(8), + TYPE_ANNOTATIONS(8), + INTERSECTION_TYPES_IN_CASTS(8), + LAMBDA_EXPRESSIONS(8), + METHOD_REFERENCES(8), + + MODULE_DECLARATIONS(9), + DIAMOND_TYPE_ARGUMENTS_FOR_ANONYMOUS_CLASSES(9), + PRIVATE_METHODS_IN_INTERFACES(9), + CONCISE_RESOURCE_SYNTAX(9); + + private final int minJdkLevel; + + RegularLanguageFeature(int minJdkLevel) { + this.minJdkLevel = minJdkLevel; + } + + + @Override + public String errorMessage(int jdk, boolean preview) { + if (jdk >= this.minJdkLevel) { + return null; + } + return StringUtils.capitalize(displayNameLower(name())) + + " are a feature of " + versionDisplayName(minJdkLevel) + + ", you should select your language version accordingly"; + } + + } + + interface LanguageFeature { + + @Nullable + String errorMessage(int jdk, boolean preview); + } private class CheckVisitor extends SideEffectingVisitorAdapter { @Override public void visit(ASTStringLiteral node, T data) { - if (jdkVersion != 13 || !preview) { - if (node.isTextBlock()) { - check(node, PreviewFeature.TEXT_BLOCK_LITERALS, data); - } + if (node.isStringLiteral() && SPACE_ESCAPE_PATTERN.matcher(node.getImage()).find()) { + check(node, PreviewFeature.SPACE_STRING_ESCAPES, data); + } + if (node.isTextBlock()) { + check(node, PreviewFeature.TEXT_BLOCK_LITERALS, data); } visitChildren(node, data); } @@ -129,6 +286,12 @@ public class LanguageLevelChecker { visitChildren(node, data); } + @Override + public void visit(ASTRecordDeclaration node, T data) { + check(node, PreviewFeature.RECORD_DECLARATIONS, data); + visitChildren(node, data); + } + @Override public void visit(ASTConstructorCall node, T data) { if (node.usesDiamondTypeArgs()) { @@ -231,6 +394,12 @@ public class LanguageLevelChecker { visitChildren(node, data); } + @Override + public void visit(ASTTypeTestPattern node, T data) { + check(node, PreviewFeature.TYPE_TEST_PATTERNS_IN_INSTANCEOF, data); + visitChildren(node, data); + } + @Override public void visit(ASTTryStatement node, T data) { if (node.isTryWithResources()) { @@ -290,10 +459,13 @@ public class LanguageLevelChecker { @Override public void visit(ASTAnyTypeDeclaration node, T data) { - if ("var".equals(node.getSimpleName())) { + String simpleName = node.getSimpleName(); + if ("var".equals(simpleName)) { check(node, ReservedIdentifiers.VAR_AS_A_TYPE_NAME, data); + } else if ("record".equals(simpleName)) { + check(node, ReservedIdentifiers.RECORD_AS_A_TYPE_NAME, data); } - checkIdent(node, node.getSimpleName(), data); + checkIdent(node, simpleName, data); visitChildren(node, data); } @@ -315,144 +487,4 @@ public class LanguageLevelChecker { } - private static String displayNameLower(String name) { - return name.replaceAll("__", "-") - .replace('_', ' ') - .toLowerCase(Locale.ROOT); - } - - - private static String versionDisplayName(int jdk) { - if (jdk < 8) { - return "Java 1." + jdk; - } else { - return "Java " + jdk; - } - } - - /** Those are hacked just for the preview features. */ - private enum PreviewFeature implements LanguageFeature { - BREAK__WITH__VALUE_STATEMENTS(12, false), - - COMPOSITE_CASE_LABEL(12, true), - SWITCH_EXPRESSIONS(12, true), - SWITCH_RULES(12, true), - - TEXT_BLOCK_LITERALS(13, false), - YIELD_STATEMENTS(13, false); - - - private final int minJdkVersion; - private final boolean alsoAbove; - - PreviewFeature(int minJdkVersion, boolean alsoAbove) { - this.minJdkVersion = minJdkVersion; - this.alsoAbove = alsoAbove; - } - - @Override - public boolean isAvailable(int jdk, boolean preview) { - return preview && (jdk == minJdkVersion || alsoAbove && jdk > minJdkVersion); - } - - - @Override - public String whenUnavailableMessage() { - return StringUtils.capitalize(displayNameLower(name())) - + " is a feature of JDK >= " + "Java " + minJdkVersion + " preview" - + ", you should select your language version accordingly"; - } - } - - /** Those use a max valid version. */ - private enum ReservedIdentifiers implements LanguageFeature { - ASSERT_AS_AN_IDENTIFIER(4, "assert"), - ENUM_AS_AN_IDENTIFIER(5, "enum"), - UNDERSCORE_AS_AN_IDENTIFIER(9, "_"), - VAR_AS_A_TYPE_NAME(10, "var"); - - private final int maxJdkVersion; - private final String reserved; - - ReservedIdentifiers(int minJdkVersion, String reserved) { - this.maxJdkVersion = minJdkVersion; - this.reserved = reserved; - } - - @Override - public boolean isAvailable(int jdk, boolean preview) { - return jdk < this.maxJdkVersion; - } - - @Override - public String whenUnavailableMessage() { - String s = displayNameLower(name()); - String usageType = s.substring(s.indexOf(' ') + 1); // eg "as an identifier" - return "Since " + LanguageLevelChecker.versionDisplayName(maxJdkVersion) + ", '" + reserved + "'" - + " is reserved and cannot be used " + usageType; - } - } - - /** Those use a min valid version. */ - private enum RegularLanguageFeature implements LanguageFeature { - - ASSERT_STATEMENTS(4), - - STATIC_IMPORT(5), - ENUMS(5), - GENERICS(5), - ANNOTATIONS(5), - FOREACH_LOOPS(5), - VARARGS_PARAMETERS(5), - HEXADECIMAL_FLOATING_POINT_LITERALS(5), - - UNDERSCORES_IN_NUMERIC_LITERALS(7), - BINARY_NUMERIC_LITERALS(7), - TRY_WITH_RESOURCES(7), - COMPOSITE_CATCH_CLAUSES(7), - DIAMOND_TYPE_ARGUMENTS(7), - - DEFAULT_METHODS(8), - RECEIVER_PARAMETERS(8), - TYPE_ANNOTATIONS(8), - INTERSECTION_TYPES_IN_CASTS(8), - LAMBDA_EXPRESSIONS(8), - METHOD_REFERENCES(8), - - MODULE_DECLARATIONS(9), - DIAMOND_TYPE_ARGUMENTS_FOR_ANONYMOUS_CLASSES(9), - PRIVATE_METHODS_IN_INTERFACES(9), - CONCISE_RESOURCE_SYNTAX(9); - - private final int minJdkLevel; - - RegularLanguageFeature(int minJdkLevel) { - this.minJdkLevel = minJdkLevel; - } - - - @Override - public boolean isAvailable(int jdk, boolean preview) { - return jdk >= this.minJdkLevel; - } - - - @Override - public String whenUnavailableMessage() { - return StringUtils.capitalize(displayNameLower(name())) - + " are a feature of " + versionDisplayName(minJdkLevel) - + ", you should select your language version accordingly"; - } - - } - - interface LanguageFeature { - - boolean isAvailable(int jdk, boolean preview); - - - String whenUnavailableMessage(); - } - - } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/internal/PrettyPrintingUtil.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/internal/PrettyPrintingUtil.java index c79eb94ddf..ca16c0d473 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/internal/PrettyPrintingUtil.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/internal/PrettyPrintingUtil.java @@ -12,6 +12,7 @@ import net.sourceforge.pmd.lang.java.ast.ASTFormalParameter; import net.sourceforge.pmd.lang.java.ast.ASTFormalParameters; import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTMethodOrConstructorDeclaration; +import net.sourceforge.pmd.lang.java.ast.ASTRecordDeclaration; /** * @author Clément Fournier @@ -70,6 +71,8 @@ public final class PrettyPrintingUtil { return "annotation"; } else if (decl instanceof ASTEnumDeclaration) { return "enum"; + } else if (decl instanceof ASTRecordDeclaration) { + return "record"; } return "class"; } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/multifile/signature/JavaOperationSignature.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/multifile/signature/JavaOperationSignature.java index 8e762249b4..d677d232a4 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/multifile/signature/JavaOperationSignature.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/multifile/signature/JavaOperationSignature.java @@ -124,6 +124,7 @@ public final class JavaOperationSignature extends JavaSignature exceptions; - private boolean ignoreAnnotations; - private static final String CLONE = "clone"; - private static final String OBJECT = "Object"; + private static final String CLONE_METHOD_NAME = "clone"; - // TODO extend AbstractIgnoredAnnotationsRule node + // TODO extend AbstractIgnoredAnnotationRule node // TODO ignore if there is javadoc - private static final PropertyDescriptor IGNORE_ANNOTATIONS_DESCRIPTOR = booleanProperty("ignoreAnnotations").defaultValue(false).desc("Ignore annotations").build(); + private static final PropertyDescriptor IGNORE_ANNOTATIONS_DESCRIPTOR = + booleanProperty("ignoreAnnotations") + .defaultValue(false) + .desc("Ignore annotations") + .build(); + private String packageName; public UselessOverridingMethodRule() { definePropertyDescriptor(IGNORE_ANNOTATIONS_DESCRIPTOR); - - exceptions = new ArrayList<>(1); - exceptions.add("CloneNotSupportedException"); } @Override - public Object visit(ASTCompilationUnit node, Object data) { - init(); - return super.visit(node, data); - } - - private void init() { - ignoreAnnotations = getProperty(IGNORE_ANNOTATIONS_DESCRIPTOR); + public void start(RuleContext ctx) { + packageName = ""; } @Override @@ -74,35 +72,33 @@ public class UselessOverridingMethodRule extends AbstractJavaRule { } // TODO: this method should be externalize into an utility class, shouldn't it ? - private boolean isMethodType(ASTMethodDeclaration node, String methodType) { - boolean result = false; + private boolean isMethodResultType(ASTMethodDeclaration node, Class resultType) { ASTResultType type = node.getResultType(); - if (type != null) { - result = type.hasDescendantMatchingXPath( - "./Type/ReferenceType/ClassOrInterfaceType[@Image = '" + methodType + "']"); + if (type != null && type.getChild(0) instanceof ASTType) { + Class resolvedResultType = ((ASTType) type.getChild(0)).getType(); + return resultType.equals(resolvedResultType); } - return result; + return false; } // TODO: this method should be externalize into an utility class, shouldn't it ? - private boolean isMethodThrowingType(ASTMethodDeclaration node, List exceptedExceptions) { - boolean result = false; - ASTNameList thrownsExceptions = node.getFirstChildOfType(ASTNameList.class); + private boolean isMethodThrowingType(ASTMethodDeclaration node, Class exceptionType) { + @Nullable ASTThrowsList thrownsExceptions = node.getThrowsList(); if (thrownsExceptions != null) { List names = thrownsExceptions.findChildrenOfType(ASTName.class); for (ASTName name : names) { - for (String exceptedException : exceptedExceptions) { - if (exceptedException.equals(name.getImage())) { - result = true; - } + if (name.getType() != null && name.getType() == exceptionType) { + return true; } } } - return result; + return false; } - private boolean hasArguments(ASTMethodDeclaration node) { - return node.hasDescendantMatchingXPath("./MethodDeclarator/FormalParameters/*"); + @Override + public Object visit(ASTPackageDeclaration node, Object data) { + packageName = node.getPackageNameImage(); + return super.visit(node, data); } @Override @@ -116,8 +112,7 @@ public class UselessOverridingMethodRule extends AbstractJavaRule { // We can also skip the 'clone' method as they are generally // 'useless' but as it is considered a 'good practice' to // implement them anyway ( see bug 1522517) - if (CLONE.equals(node.getName()) && node.isPublic() && !this.hasArguments(node) - && this.isMethodType(node, OBJECT) && this.isMethodThrowingType(node, exceptions)) { + if (isCloneMethod(node)) { return super.visit(node, data); } @@ -140,47 +135,47 @@ public class UselessOverridingMethodRule extends AbstractJavaRule { if (statementGrandChild instanceof ASTPrimaryExpression) { primaryExpression = (ASTPrimaryExpression) statementGrandChild; } else { - List primaryExpressions = findFirstDegreeChildrenOfType(statementGrandChild, - ASTPrimaryExpression.class); + List primaryExpressions = statementGrandChild + .findChildrenOfType(ASTPrimaryExpression.class); if (primaryExpressions.size() != 1) { return super.visit(node, data); } primaryExpression = primaryExpressions.get(0); } - ASTPrimaryPrefix primaryPrefix = findFirstDegreeChildrenOfType(primaryExpression, ASTPrimaryPrefix.class) - .get(0); + ASTPrimaryPrefix primaryPrefix = primaryExpression.getFirstChildOfType(ASTPrimaryPrefix.class); if (!primaryPrefix.usesSuperModifier()) { return super.visit(node, data); } - List primarySuffixList = findFirstDegreeChildrenOfType(primaryExpression, - ASTPrimarySuffix.class); + List primarySuffixList = primaryExpression.findChildrenOfType(ASTPrimarySuffix.class); if (primarySuffixList.size() != 2) { // extra method call on result of super method return super.visit(node, data); } - ASTMethodDeclarator methodDeclarator = findFirstDegreeChildrenOfType(node, ASTMethodDeclarator.class).get(0); ASTPrimarySuffix primarySuffix = primarySuffixList.get(0); - if (!primarySuffix.hasImageEqualTo(methodDeclarator.getImage())) { + if (!primarySuffix.hasImageEqualTo(node.getName())) { return super.visit(node, data); } // Process arguments primarySuffix = primarySuffixList.get(1); ASTArguments arguments = (ASTArguments) primarySuffix.getChild(0); - ASTFormalParameters formalParameters = (ASTFormalParameters) methodDeclarator.getChild(0); + ASTFormalParameters formalParameters = node.getFormalParameters(); if (formalParameters.getNumChildren() != arguments.getNumChildren()) { return super.visit(node, data); } - if (!ignoreAnnotations && node.getDeclaredAnnotations().any(it -> !TypeHelper.isExactlyA(it, Override.class.getName()))) { + if (!getProperty(IGNORE_ANNOTATIONS_DESCRIPTOR) && node.getDeclaredAnnotations().any(it -> !TypeHelper.isExactlyA(it, Override.class.getName()))) { return super.visit(node, data); } - if (arguments.getNumChildren() == 0) { - addViolation(data, node, getMessage()); - } else { + // different number of args + if (arguments.size() != node.getArity()) { + return super.visit(node, data); + } + + if (arguments.size() > 0) { ASTArgumentList argumentList = (ASTArgumentList) arguments.getChild(0); for (int i = 0; i < argumentList.getNumChildren(); i++) { Node expressionChild = argumentList.getChild(i).getChild(0); @@ -201,51 +196,89 @@ public class UselessOverridingMethodRule extends AbstractJavaRule { return super.visit(node, data); } - if (formalParameters.getNumChildren() < i + 1) { - return super.visit(node, data); // different number of args - } - ASTName argumentName = (ASTName) argumentPrimaryPrefixChild; ASTFormalParameter formalParameter = (ASTFormalParameter) formalParameters.getChild(i); - ASTVariableDeclaratorId variableId = findFirstDegreeChildrenOfType(formalParameter, - ASTVariableDeclaratorId.class).get(0); + ASTVariableDeclaratorId variableId = formalParameter.getFirstChildOfType(ASTVariableDeclaratorId.class); if (!argumentName.hasImageEqualTo(variableId.getImage())) { // The arguments are not simply passed through return super.visit(node, data); } - } - // All arguments are passed through directly - addViolation(data, node, getMessage()); } + + if (modifiersChanged(node)) { + return super.visit(node, data); + } + + // All arguments are passed through directly or there were no arguments + addViolation(data, node, getMessage()); + return super.visit(node, data); } - public List findFirstDegreeChildrenOfType(Node n, Class targetType) { - List l = new ArrayList<>(); - lclFindChildrenOfType(n, targetType, l); - return l; + private boolean isCloneMethod(ASTMethodDeclaration node) { + boolean isCloneAndPublic = CLONE_METHOD_NAME.equals(node.getName()) && node.isPublic(); + boolean hasNoParameters = node.getArity() == 0; + return isCloneAndPublic + && hasNoParameters + && this.isMethodResultType(node, Object.class) + && this.isMethodThrowingType(node, CloneNotSupportedException.class); } - private void lclFindChildrenOfType(Node node, Class targetType, List results) { - if (node.getClass().equals(targetType)) { - results.add((T) node); + private boolean modifiersChanged(ASTMethodDeclaration node) { + Class type = node.getFirstParentOfType(ASTClassOrInterfaceDeclaration.class).getType(); + if (type == null) { + return false; } - if (node instanceof ASTClassOrInterfaceDeclaration && ((ASTClassOrInterfaceDeclaration) node).isNested()) { - return; - } + String overriddenMethodName = node.getName(); - if (node instanceof ASTClassOrInterfaceBodyDeclaration - && ((ASTClassOrInterfaceBodyDeclaration) node).isAnonymousInnerClass()) { - return; - } - - for (int i = 0; i < node.getNumChildren(); i++) { - Node child = node.getChild(i); - if (child.getClass().equals(targetType)) { - results.add((T) child); + List> typeArguments = new ArrayList<>(); + for (ASTFormalParameter parameter : node.getFormalParameters()) { + Class parameterType = parameter.getType(); + if (parameterType != null) { + typeArguments.add(parameterType); } } + + // did we have for each parameter the type? + if (typeArguments.size() != node.getFormalParameters().size()) { + return false; + } + + // search method with same name up the hierarchy + Class[] typeArgumentArray = typeArguments.toArray(new Class[0]); + Class superType = type.getSuperclass(); + Method declaredMethod = null; + while (superType != null && declaredMethod == null) { + try { + declaredMethod = superType.getDeclaredMethod(overriddenMethodName, typeArgumentArray); + } catch (NoSuchMethodException | SecurityException e) { + declaredMethod = null; + } + superType = superType.getSuperclass(); + } + + return declaredMethod != null && isElevatingAccessModifier(node, declaredMethod); } + + private boolean isElevatingAccessModifier(ASTMethodDeclaration overridingMethod, Method superMethod) { + String superPackageName = null; + Package p = superMethod.getDeclaringClass().getPackage(); + if (p != null) { + superPackageName = p.getName(); + } + // Note: can't simply compare superMethod.getModifiers() with overridingMethod.getModifiers() + // since AccessNode#PROTECTED != Modifier#PROTECTED. + boolean elevatingFromProtected = Modifier.isProtected(superMethod.getModifiers()) + && !overridingMethod.isProtected(); + boolean elevatingFromPackagePrivate = superMethod.getModifiers() == 0 + && !overridingMethod.getModifiers().getExplicitModifiers().isEmpty(); + boolean elevatingIntoDifferentPackage = !packageName.equals(superPackageName); + + return elevatingFromProtected + || elevatingFromPackagePrivate + || elevatingIntoDifferentPackage; + } + } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symboltable/ScopeAndDeclarationFinder.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symboltable/ScopeAndDeclarationFinder.java index 75750f83fb..4810665029 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symboltable/ScopeAndDeclarationFinder.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symboltable/ScopeAndDeclarationFinder.java @@ -20,6 +20,7 @@ import net.sourceforge.pmd.lang.java.ast.ASTImportDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTLambdaExpression; import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTPackageDeclaration; +import net.sourceforge.pmd.lang.java.ast.ASTRecordDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTSwitchStatement; import net.sourceforge.pmd.lang.java.ast.ASTTryStatement; import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId; @@ -187,6 +188,13 @@ public class ScopeAndDeclarationFinder extends JavaParserVisitorAdapter { return data; } + @Override + public Object visit(ASTRecordDeclaration node, Object data) { + createClassScope(node); + cont(node); + return data; + } + @Override public Object visit(ASTAnonymousClassDeclaration node, Object data) { createClassScope(node); @@ -257,6 +265,16 @@ public class ScopeAndDeclarationFinder extends JavaParserVisitorAdapter { @Override public Object visit(ASTVariableDeclaratorId node, Object data) { + if (node.isPatternBinding()) { + // Don't consider type test patterns here. It could bind to a name + // that is already in the scope (e.g. field names). + // type tests patterns are tricky to implement + // and given it's a preview feature, and the sym table will be replaced + // for 7.0, it's not useful to support them. + // See https://cr.openjdk.java.net/~briangoetz/amber/pattern-semantics.html#scoping-of-pattern-variables + return super.visit(node, data); + } + VariableNameDeclaration decl = new VariableNameDeclaration(node); node.getScope().addDeclaration(decl); node.setNameDeclaration(decl); diff --git a/pmd-java/src/main/resources/category/java/errorprone.xml b/pmd-java/src/main/resources/category/java/errorprone.xml index 01fcb21ca0..edc4995f84 100644 --- a/pmd-java/src/main/resources/category/java/errorprone.xml +++ b/pmd-java/src/main/resources/category/java/errorprone.xml @@ -2244,7 +2244,7 @@ public class MyClass { @@ -2253,26 +2253,33 @@ Either the check is useless (the variable will never be "null") or it is incorre 3 + @@ -2281,8 +2288,10 @@ Either the check is useless (the variable will never be "null") or it is incorre @@ -2290,7 +2299,9 @@ public class Foo { diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/LanguageVersionDiscovererTest.java b/pmd-java/src/test/java/net/sourceforge/pmd/LanguageVersionDiscovererTest.java index a4fb4a9822..98c660c09e 100644 --- a/pmd-java/src/test/java/net/sourceforge/pmd/LanguageVersionDiscovererTest.java +++ b/pmd-java/src/test/java/net/sourceforge/pmd/LanguageVersionDiscovererTest.java @@ -26,8 +26,8 @@ public class LanguageVersionDiscovererTest { File javaFile = new File("/path/to/MyClass.java"); LanguageVersion languageVersion = discoverer.getDefaultLanguageVersionForFile(javaFile); - assertEquals("LanguageVersion must be Java 13 !", - LanguageRegistry.getLanguage(JavaLanguageModule.NAME).getVersion("13"), languageVersion); + assertEquals("LanguageVersion must be Java 14 !", + LanguageRegistry.getLanguage(JavaLanguageModule.NAME).getVersion("14"), languageVersion); } /** @@ -48,7 +48,7 @@ public class LanguageVersionDiscovererTest { public void testLanguageVersionDiscoverer() { PMDConfiguration configuration = new PMDConfiguration(); LanguageVersionDiscoverer languageVersionDiscoverer = configuration.getLanguageVersionDiscoverer(); - assertEquals("Default Java version", LanguageRegistry.getLanguage(JavaLanguageModule.NAME).getVersion("13"), + assertEquals("Default Java version", LanguageRegistry.getLanguage(JavaLanguageModule.NAME).getVersion("14"), languageVersionDiscoverer .getDefaultLanguageVersion(LanguageRegistry.getLanguage(JavaLanguageModule.NAME))); configuration diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/LanguageVersionTest.java b/pmd-java/src/test/java/net/sourceforge/pmd/LanguageVersionTest.java index 406fe585ac..7c217d0a25 100644 --- a/pmd-java/src/test/java/net/sourceforge/pmd/LanguageVersionTest.java +++ b/pmd-java/src/test/java/net/sourceforge/pmd/LanguageVersionTest.java @@ -20,7 +20,7 @@ public class LanguageVersionTest extends AbstractLanguageVersionTest { } @Parameters - public static Collection data() { + public static Collection data() { return Arrays.asList(new Object[][] { { JavaLanguageModule.NAME, JavaLanguageModule.TERSE_NAME, "1.3", LanguageRegistry.getLanguage(JavaLanguageModule.NAME).getVersion("1.3"), }, @@ -48,6 +48,10 @@ public class LanguageVersionTest extends AbstractLanguageVersionTest { LanguageRegistry.getLanguage(JavaLanguageModule.NAME).getVersion("13"), }, { JavaLanguageModule.NAME, JavaLanguageModule.TERSE_NAME, "13-preview", LanguageRegistry.getLanguage(JavaLanguageModule.NAME).getVersion("13-preview"), }, + { JavaLanguageModule.NAME, JavaLanguageModule.TERSE_NAME, "14", + LanguageRegistry.getLanguage(JavaLanguageModule.NAME).getVersion("14"), }, + { JavaLanguageModule.NAME, JavaLanguageModule.TERSE_NAME, "14-preview", + LanguageRegistry.getLanguage(JavaLanguageModule.NAME).getVersion("14-preview"), }, // this one won't be found: case sensitive! { "JAVA", "JAVA", "1.7", null, }, }); diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/Java12Test.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/Java12Test.java deleted file mode 100644 index b80cb380c6..0000000000 --- a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/Java12Test.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * BSD-style license; for more info see http://pmd.sourceforge.net/license.html - */ - -package net.sourceforge.pmd.lang.java.ast; - -import org.junit.Ignore; -import org.junit.Test; - -import net.sourceforge.pmd.lang.ast.ParseException; -import net.sourceforge.pmd.lang.java.JavaParsingHelper; - -@Ignore("those tests depend on type resolution") -public class Java12Test { - - - private final JavaParsingHelper java11 = - JavaParsingHelper.WITH_PROCESSING.withDefaultVersion("11") - .withResourceContext(Java12Test.class, "jdkversiontests/java12/"); - - - @Test(expected = ParseException.class) - public void testMultipleCaseLabelsJava11() { - java11.parseResource("MultipleCaseLabels.java"); - } - - @Test(expected = ParseException.class) - public void testSwitchRulesJava11() { - java11.parseResource("SwitchRules.java"); - } - - - @Test(expected = ParseException.class) - public void testSwitchExpressionsJava11() { - java11.parseResource("SwitchExpressions.java"); - } - -} diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/Java13Test.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/Java13Test.java index ec05ed3182..fec0c374a4 100644 --- a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/Java13Test.java +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/Java13Test.java @@ -19,17 +19,10 @@ public class Java13Test { private final JavaParsingHelper java12 = JavaParsingHelper.WITH_PROCESSING.withDefaultVersion("12") - .withResourceContext(Java13Test.class, "jdkversiontests/java13/"); + .withResourceContext(getClass(), "jdkversiontests/java13/"); private final JavaParsingHelper java13p = java12.withDefaultVersion("13-preview"); - - - @Test(expected = ParseException.class) - public void testSwitchExpressionsBeforeJava13() { - java12.parseResource("SwitchExpressions.java"); - } - @Test public void testTextBlocks() { ASTCompilationUnit compilationUnit = java13p.parseResource("TextBlocks.java"); diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/Java14PreviewTest.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/Java14PreviewTest.java new file mode 100644 index 0000000000..ab1d803b04 --- /dev/null +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/Java14PreviewTest.java @@ -0,0 +1,160 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + + +package net.sourceforge.pmd.lang.java.ast; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import net.sourceforge.pmd.lang.ast.ParseException; +import net.sourceforge.pmd.lang.java.JavaParsingHelper; + +/** + * Tests new java14 preview features. + */ +public class Java14PreviewTest { + private final JavaParsingHelper java14 = + JavaParsingHelper.WITH_PROCESSING.withDefaultVersion("14") + .withResourceContext(getClass(), "jdkversiontests/java14/"); + + private final JavaParsingHelper java14p = java14.withDefaultVersion("14-preview"); + private final JavaParsingHelper java13 = java14.withDefaultVersion("13"); + + @Test + public void textBlocks() { + ASTCompilationUnit compilationUnit = java14p.parseResource("TextBlocks.java"); + List literals = compilationUnit.findDescendantsOfType(ASTStringLiteral.class); + Assert.assertEquals(22, literals.size()); + Assert.assertFalse(literals.get(2).isTextBlock()); + Assert.assertFalse(literals.get(12).isTextBlock()); + Assert.assertFalse(literals.get(17).isTextBlock()); + Assert.assertFalse(literals.get(18).isTextBlock()); + Assert.assertFalse(literals.get(20).isTextBlock()); + Assert.assertFalse(literals.get(21).isTextBlock()); + + List textBlocks = new ArrayList<>(); + for (ASTStringLiteral literal : literals) { + if (literal.isTextBlock()) { + textBlocks.add(literal); + } + } + Assert.assertEquals(16, textBlocks.size()); + Assert.assertEquals("\"\"\"\n" + + " \n" + + " \n" + + "

Hello, world

\n" + + " \n" + + " \n" + + " \"\"\"", + textBlocks.get(0).getImage()); + Assert.assertEquals("\n" + + " \n" + + "

Hello, world

\n" + + " \n" + + "\n", textBlocks.get(0).getConstValue()); + + // Note: More tests are in ASTLiteralTest. + } + + @Test(expected = ParseException.class) + public void textBlocksBeforeJava14PreviewShouldFail() { + java13.parseResource("TextBlocks.java"); + } + + @Test(expected = ParseException.class) + public void stringEscapeSequenceShouldFail() { + java14.parse("class Foo { String s =\"a\\sb\"; }"); + } + + @Test + public void recordPoint() { + ASTCompilationUnit compilationUnit = java14p.parseResource("Point.java"); + ASTRecordDeclaration recordDecl = compilationUnit.getFirstDescendantOfType(ASTRecordDeclaration.class); + Assert.assertEquals("Point", recordDecl.getImage()); + Assert.assertFalse(recordDecl.isNested()); + List components = recordDecl.getFirstChildOfType(ASTRecordComponentList.class) + .findChildrenOfType(ASTRecordComponent.class); + Assert.assertEquals(2, components.size()); + Assert.assertEquals("x", components.get(0).getVarId().getImage()); + Assert.assertEquals("y", components.get(1).getVarId().getImage()); + } + + @Test(expected = ParseException.class) + public void recordPointBeforeJava14PreviewShouldFail() { + java14.parseResource("Point.java"); + } + + @Test(expected = ParseException.class) + public void recordCtorWithThrowsShouldFail() { + java14p.parse(" record R {" + + " R throws IOException {}" + + " }"); + } + + @Test + public void innerRecords() { + ASTCompilationUnit compilationUnit = java14p.parseResource("Records.java"); + List recordDecls = compilationUnit.findDescendantsOfType(ASTRecordDeclaration.class, true); + Assert.assertEquals(7, recordDecls.size()); + + ASTRecordDeclaration complex = recordDecls.get(0); + Assert.assertEquals("MyComplex", complex.getSimpleName()); + Assert.assertTrue(complex.isNested()); + Assert.assertEquals(0, getComponent(complex, 0).getDeclaredAnnotations().count()); + Assert.assertEquals(1, getComponent(complex, 1).getDeclaredAnnotations().count()); + Assert.assertEquals(2, complex.getDeclarations().count()); + Assert.assertTrue(complex.getDeclarations().get(0).getDeclarationNode() instanceof ASTConstructorDeclaration); + Assert.assertTrue(complex.getDeclarations().get(1).getDeclarationNode() instanceof ASTRecordDeclaration); + + ASTRecordDeclaration nested = recordDecls.get(1); + Assert.assertEquals("Nested", nested.getSimpleName()); + Assert.assertTrue(nested.isNested()); + + ASTRecordDeclaration range = recordDecls.get(2); + Assert.assertEquals("Range", range.getSimpleName()); + Assert.assertEquals(2, range.getComponentList().size()); + List rangeConstructors = range.findDescendantsOfType(ASTRecordConstructorDeclaration.class); + Assert.assertEquals(1, rangeConstructors.size()); + Assert.assertEquals("Range", rangeConstructors.get(0).getImage()); + JavaNode mods = rangeConstructors.get(0).getChild(0); + Assert.assertTrue(mods instanceof ASTModifierList); + Assert.assertEquals(1, mods.getNumChildren()); + Assert.assertEquals(2, range.getDeclarations().count()); + + ASTRecordDeclaration varRec = recordDecls.get(3); + Assert.assertEquals("VarRec", varRec.getSimpleName()); + Assert.assertEquals("x", getComponent(varRec, 0).getVarId().getImage()); + Assert.assertTrue(getComponent(varRec, 0).isVarargs()); + Assert.assertEquals(2, getComponent(varRec, 0).getDeclaredAnnotations().count()); + Assert.assertEquals(1, getComponent(varRec, 0).getTypeNode().descendants(ASTAnnotation.class).count()); + + ASTRecordDeclaration arrayRec = recordDecls.get(4); + Assert.assertEquals("ArrayRec", arrayRec.getSimpleName()); + Assert.assertEquals("x", getComponent(arrayRec, 0).getVarId().getImage()); + Assert.assertTrue(getComponent(arrayRec, 0).getVarId().hasArrayType()); + + ASTRecordDeclaration emptyRec = recordDecls.get(5); + Assert.assertEquals("EmptyRec", emptyRec.getSimpleName()); + Assert.assertEquals(0, emptyRec.getComponentList().size()); + + ASTRecordDeclaration personRec = recordDecls.get(6); + Assert.assertEquals("PersonRecord", personRec.getSimpleName()); + ASTImplementsList impl = personRec.getFirstChildOfType(ASTImplementsList.class); + Assert.assertEquals(2, impl.findChildrenOfType(ASTClassOrInterfaceType.class).size()); + } + + private ASTRecordComponent getComponent(ASTRecordDeclaration arrayRec, int index) { + return arrayRec.getComponentList().getChild(index); + } + + + @Test(expected = ParseException.class) + public void recordIsARestrictedIdentifier() { + java14p.parse("public class record {}"); + } +} diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/TextBlockEscapeTest.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/TextBlockEscapeTest.java new file mode 100644 index 0000000000..cdedc88f8e --- /dev/null +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/TextBlockEscapeTest.java @@ -0,0 +1,123 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.java.ast; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class TextBlockEscapeTest extends BaseParserTest { + + @Test + public void testTextBlockContent() { + assertEquals("empty text block", "", + ASTStringLiteral.determineTextBlockContent("\"\"\"\n \"\"\"")); + assertEquals("single line text block", "winter", + ASTStringLiteral.determineTextBlockContent("\"\"\"\n winter\"\"\"")); + assertEquals("single line text block with LF", "winter\n", + ASTStringLiteral.determineTextBlockContent("\"\"\"\n" + + " winter\n" + + " \"\"\"")); + assertEquals("basic text block example with html", + "\n" + + " \n" + + "

Hello, world

\n" + + " \n" + + "\n", + ASTStringLiteral.determineTextBlockContent("\"\"\"\n" + + " \n" + + " \n" + + "

Hello, world

\n" + + " \n" + + " \n" + + " \"\"\"")); + assertEquals("text block with escapes", + "\r\n" + + " \r\n" + + "

Hello, world

\r\n" + + " \r\n" + + "\r\n", + ASTStringLiteral.determineTextBlockContent("\"\"\"\n" + + " \\r\n" + + " \\r\n" + + "

Hello, world

\\r\n" + + " \\r\n" + + " \\r\n" + + " \"\"\"")); + assertEquals("escaped text block in inside text block", + "String text = \"\"\"\n" + + " A text block inside a text block\n" + + "\"\"\";\n", + ASTStringLiteral.determineTextBlockContent("\"\"\"\n" + + " String text = \\\"\"\"\n" + + " A text block inside a text block\n" + + " \\\"\"\";\n" + + " \"\"\"")); + assertEquals("new escape: line continuation", + "Lorem ipsum dolor sit amet, consectetur adipiscing " + + "elit, sed do eiusmod tempor incididunt ut labore " + + "et dolore magna aliqua.", + ASTStringLiteral.determineTextBlockContent("\"\"\"\n" + + " Lorem ipsum dolor sit amet, consectetur adipiscing \\\n" + + " elit, sed do eiusmod tempor incididunt ut labore \\\n" + + " et dolore magna aliqua.\\\n" + + " \"\"\"")); + assertEquals("new escape: space escape", + "red \n" + + "green \n" + + "blue \n", + ASTStringLiteral.determineTextBlockContent("\"\"\"\n" + + " red \\s\n" + + " green\\s\n" + + " blue \\s\n" + + " \"\"\"")); + assertEquals("with crlf line endings", + "\n" + + " \n" + + "

Hello, world

\n" + + " \n" + + "\n", ASTStringLiteral.determineTextBlockContent("\"\"\"\r\n" + + " \r\n" + + " \r\n" + + "

Hello, world

\r\n" + + " \r\n" + + " \r\n" + + " \"\"\"")); + assertEquals("with cr line endings", + "\n" + + " \n" + + "

Hello, world

\n" + + " \n" + + "\n", ASTStringLiteral.determineTextBlockContent("\"\"\"\r" + + " \r" + + " \r" + + "

Hello, world

\r" + + " \r" + + " \r" + + " \"\"\"")); + assertEquals("empty line directly after opening", + "\ntest\n", ASTStringLiteral.determineTextBlockContent("\"\"\"\n" + + " \n" + + " test\n" + + " \"\"\"")); + assertEquals("empty crlf line directly after opening", + "\ntest\n", ASTStringLiteral.determineTextBlockContent("\"\"\"\r\n" + + " \r\n" + + " test\r\n" + + " \"\"\"")); + assertEquals("empty line directly after opening without indentation", + "\ntest\n", ASTStringLiteral.determineTextBlockContent("\"\"\"\n" + + "\n" + + "test\n" + + "\"\"\"")); + assertEquals("empty crlf line directly after opening without indentation", + "\ntest\n", ASTStringLiteral.determineTextBlockContent("\"\"\"\r\n" + + "\r\n" + + "test\r\n" + + "\"\"\"")); + assertEquals("text block with backslash escape", "\\test\n", + ASTStringLiteral.determineTextBlockContent("\"\"\"\n \\\\test\n \"\"\"")); + } +} diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/BaseClass.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/BaseClass.java new file mode 100644 index 0000000000..61c3799caa --- /dev/null +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/BaseClass.java @@ -0,0 +1,16 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.java.rule.design.uselessoverridingmethod; + +public class BaseClass { + + + protected void doBase() { + } + + + protected void doBaseWithArg(String foo) { + } +} diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/DirectSubclass.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/DirectSubclass.java new file mode 100644 index 0000000000..ed73905e7b --- /dev/null +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/DirectSubclass.java @@ -0,0 +1,20 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.java.rule.design.uselessoverridingmethod; + +public class DirectSubclass extends BaseClass { + + + @Override + public void doBase() { + super.doBase(); + } + + + @Override + public void doBaseWithArg(String foo) { + super.doBaseWithArg(foo); + } +} diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/DirectSubclass2.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/DirectSubclass2.java new file mode 100644 index 0000000000..4a282aeba7 --- /dev/null +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/DirectSubclass2.java @@ -0,0 +1,12 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.java.rule.design.uselessoverridingmethod; + +public class DirectSubclass2 extends DirectSubclass { + @Override + public void doBase() { + super.doBase(); + } +} diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/DirectSynchronizingSubclass.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/DirectSynchronizingSubclass.java new file mode 100644 index 0000000000..284e0224b4 --- /dev/null +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/DirectSynchronizingSubclass.java @@ -0,0 +1,14 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.java.rule.design.uselessoverridingmethod; + +public class DirectSynchronizingSubclass extends BaseClass { + + @Override + protected synchronized void doBase() { + // overriding for synchronized + super.doBase(); + } +} diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/ExposingSerializer.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/ExposingSerializer.java new file mode 100644 index 0000000000..6d7de02b26 --- /dev/null +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/ExposingSerializer.java @@ -0,0 +1,31 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.java.rule.design.uselessoverridingmethod; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +import org.w3c.dom.Node; + +class ExposingSerializer extends Serializer { + + + ExposingSerializer(OutputStream out, String encoding) throws UnsupportedEncodingException { + super(out, encoding); + } + + + /** + * Overriding in order to change the access modifier from protected to public - so: not only merely calling super. + * + *

Method signature in super class: protected void writeChild(nu.xom.Node arg0) throws java.io.IOException; + * + *

See: https://sourceforge.net/tracker/?func=detail&aid=1415525&group_id=56262&atid=479921 + */ + public void writeChild(Node node) throws IOException { + super.writeChild(node); + } +} diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/OtherSubclass.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/OtherSubclass.java new file mode 100644 index 0000000000..4f8c9d23c6 --- /dev/null +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/OtherSubclass.java @@ -0,0 +1,8 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.java.rule.design.uselessoverridingmethod; + +public class OtherSubclass extends BaseClass { +} diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/Serializer.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/Serializer.java new file mode 100644 index 0000000000..614b8940d9 --- /dev/null +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/Serializer.java @@ -0,0 +1,20 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.java.rule.design.uselessoverridingmethod; + +import java.io.IOException; +import java.io.OutputStream; + +import org.w3c.dom.Node; + +public class Serializer { + + public Serializer(OutputStream out, String encoding) { + } + + protected void writeChild(Node node) throws IOException { + } + +} diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/TransitiveSubclass.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/TransitiveSubclass.java new file mode 100644 index 0000000000..050665ec8e --- /dev/null +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/TransitiveSubclass.java @@ -0,0 +1,13 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.java.rule.design.uselessoverridingmethod; + +public class TransitiveSubclass extends OtherSubclass { + + @Override + public void doBase() { + super.doBase(); + } +} diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/other/DirectSubclassInOtherPackage.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/other/DirectSubclassInOtherPackage.java new file mode 100644 index 0000000000..0585d7e4dd --- /dev/null +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/other/DirectSubclassInOtherPackage.java @@ -0,0 +1,15 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.java.rule.design.uselessoverridingmethod.other; + +import net.sourceforge.pmd.lang.java.rule.design.uselessoverridingmethod.BaseClass; + +public class DirectSubclassInOtherPackage extends BaseClass { + + @Override + protected void doBase() { + super.doBase(); + } +} diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/other/OtherClassInOtherPackage.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/other/OtherClassInOtherPackage.java new file mode 100644 index 0000000000..80ec2a66d9 --- /dev/null +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/design/uselessoverridingmethod/other/OtherClassInOtherPackage.java @@ -0,0 +1,16 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.java.rule.design.uselessoverridingmethod.other; + +public class OtherClassInOtherPackage { + + + public void foo() { + DirectSubclassInOtherPackage instance = new DirectSubclassInOtherPackage(); + // this call is only possible, because DirectSubclassInOtherPackage makes this + // method available in this package as well. + instance.doBase(); + } +} diff --git a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/ASTLiteralTest.kt b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/ASTLiteralTest.kt index 32dd65d3d0..c9befe19e5 100644 --- a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/ASTLiteralTest.kt +++ b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/ASTLiteralTest.kt @@ -120,7 +120,7 @@ $delim } - parserTest("Text block literal on non-JDK13 preview", javaVersions = !J13__PREVIEW) { + parserTest("Text block literal on non-JDK13 preview", javaVersions = JavaVersion.except(J13__PREVIEW, J14__PREVIEW)) { val delim = "\"\"\"" diff --git a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/ASTPatternTest.kt b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/ASTPatternTest.kt new file mode 100644 index 0000000000..959a8bc6b0 --- /dev/null +++ b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/ASTPatternTest.kt @@ -0,0 +1,40 @@ +package net.sourceforge.pmd.lang.java.ast + +import io.kotlintest.matchers.string.shouldContain +import net.sourceforge.pmd.lang.ast.test.shouldBe +import net.sourceforge.pmd.lang.java.ast.JavaVersion.J14__PREVIEW +import java.io.IOException + +class ASTPatternTest : ParserTestSpec({ + + parserTest("Test patterns only available on JDK 14 (preview)", javaVersions = !J14__PREVIEW) { + + inContext(ExpressionParsingCtx) { + "obj instanceof Class c" should throwParseException { + it.message.shouldContain("Type test patterns in instanceof is a preview feature of JDK 14, you should select your language version accordingly") + } + } + + } + + parserTest("Test simple patterns", javaVersion = J14__PREVIEW) { + + importedTypes += IOException::class.java + inContext(ExpressionParsingCtx) { + + "obj instanceof Class c" should parseAs { + infixExpr(BinaryOp.INSTANCEOF) { + variableAccess("obj") + child { + it::getPattern shouldBe child { + it::getTypeNode shouldBe classType("Class") + it::getVarId shouldBe variableId("c") + } + } + } + } + } + } + + +}) diff --git a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/ASTSwitchExpressionTests.kt b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/ASTSwitchExpressionTests.kt index 43218ccb46..17465ba96f 100644 --- a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/ASTSwitchExpressionTests.kt +++ b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/ASTSwitchExpressionTests.kt @@ -7,10 +7,9 @@ package net.sourceforge.pmd.lang.java.ast import io.kotlintest.shouldBe import net.sourceforge.pmd.lang.ast.test.shouldBe import net.sourceforge.pmd.lang.java.ast.BinaryOp.* +import net.sourceforge.pmd.lang.java.ast.JavaVersion.* import net.sourceforge.pmd.lang.java.ast.JavaVersion.Companion.Earliest import net.sourceforge.pmd.lang.java.ast.JavaVersion.Companion.Latest -import net.sourceforge.pmd.lang.java.ast.JavaVersion.J12__PREVIEW -import net.sourceforge.pmd.lang.java.ast.JavaVersion.J13__PREVIEW import net.sourceforge.pmd.lang.java.ast.UnaryOp.UNARY_MINUS @@ -19,8 +18,10 @@ import net.sourceforge.pmd.lang.java.ast.UnaryOp.UNARY_MINUS */ class ASTSwitchExpressionTests : ParserTestSpec({ + val switchVersions = listOf(J13__PREVIEW, J14, J14__PREVIEW) + val notSwitchVersions = JavaVersion.except(switchVersions) - parserTest("No switch expr before j12 preview", javaVersions = !J12__PREVIEW) { + parserTest("No switch expr before j13 preview", javaVersions = notSwitchVersions) { inContext(ExpressionParsingCtx) { @@ -40,22 +41,8 @@ class ASTSwitchExpressionTests : ParserTestSpec({ } - parserTest("No yield stmt before j13 preview", javaVersions = !J13__PREVIEW) { - inContext(ExpressionParsingCtx) { - """ - switch (day) { - default -> { - yield result * 4; - } - } - """ shouldNot parse() - } - - } - - - parserTest("Simple switch expressions", javaVersions = listOf(J13__PREVIEW)) { + parserTest("Simple switch expressions", javaVersions = switchVersions) { inContext(ExpressionParsingCtx) { @@ -117,7 +104,7 @@ class ASTSwitchExpressionTests : ParserTestSpec({ - parserTest("Non-trivial labels", javaVersions = listOf(J12__PREVIEW, J13__PREVIEW)) { + parserTest("Non-trivial labels", javaVersions = switchVersions) { inContext(ExpressionParsingCtx) { @@ -150,7 +137,7 @@ class ASTSwitchExpressionTests : ParserTestSpec({ } } - parserTest("Switch expr precedence", javaVersions = listOf(J12__PREVIEW, J13__PREVIEW)) { + parserTest("Switch expr precedence", javaVersions = switchVersions) { inContext(ExpressionParsingCtx) { @@ -184,7 +171,7 @@ class ASTSwitchExpressionTests : ParserTestSpec({ } - parserTest("Nested switch expressions", javaVersions = listOf(J12__PREVIEW, J13__PREVIEW)) { + parserTest("Nested switch expressions", javaVersions = switchVersions) { inContext(ExpressionParsingCtx) { @@ -244,7 +231,7 @@ class ASTSwitchExpressionTests : ParserTestSpec({ } - parserTest("Non-fallthrough nested in fallthrough", javaVersions = listOf(J12__PREVIEW, J13__PREVIEW)) { + parserTest("Non-fallthrough nested in fallthrough", javaVersions = switchVersions) { inContext(StatementParsingCtx) { @@ -300,7 +287,7 @@ class ASTSwitchExpressionTests : ParserTestSpec({ } - parserTest("Switch statement with non-fallthrough labels", javaVersions = listOf(J12__PREVIEW, J13__PREVIEW)) { + parserTest("Switch statement with non-fallthrough labels", javaVersions = switchVersions) { inContext(StatementParsingCtx) { """ diff --git a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/KotlinTestingDsl.kt b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/KotlinTestingDsl.kt index 51db5ca5ea..3dbc19e812 100644 --- a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/KotlinTestingDsl.kt +++ b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/ast/KotlinTestingDsl.kt @@ -10,6 +10,7 @@ import net.sourceforge.pmd.lang.ast.Node import net.sourceforge.pmd.lang.ast.ParseException import net.sourceforge.pmd.lang.ast.TokenMgrError import net.sourceforge.pmd.lang.ast.test.* +import net.sourceforge.pmd.lang.ast.test.shouldMatchNode import net.sourceforge.pmd.lang.java.JavaParsingHelper import java.beans.PropertyDescriptor @@ -17,7 +18,10 @@ import java.beans.PropertyDescriptor * Represents the different Java language versions. */ enum class JavaVersion : Comparable { - J1_3, J1_4, J1_5, J1_6, J1_7, J1_8, J9, J10, J11, J12, J12__PREVIEW, J13, J13__PREVIEW; + J1_3, J1_4, J1_5, J1_6, J1_7, J1_8, J9, J10, J11, + J12, + J13, J13__PREVIEW, + J14, J14__PREVIEW; /** Name suitable for use with e.g. [JavaParsingHelper.parse] */ val pmdName: String = name.removePrefix("J").replaceFirst("__", "-").replace('_', '.').toLowerCase() @@ -40,6 +44,10 @@ enum class JavaVersion : Comparable { companion object { val Latest = values().last() val Earliest = values().first() + + fun except(vararg versions: JavaVersion) = values().toList() - versions + fun except(versions: List) = values().toList() - versions + } } diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java12/MultipleCaseLabels.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java12/MultipleCaseLabels.java deleted file mode 100644 index b578e8c475..0000000000 --- a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java12/MultipleCaseLabels.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * - * @see JEP 325: Switch Expressions (Preview) - */ -public class MultipleCaseLabels { - private static final int MONDAY = 1; - private static final int TUESDAY = 2; - private static final int WEDNESDAY = 3; - private static final int THURSDAY = 4; - private static final int FRIDAY = 5; - private static final int SATURDAY = 6; - private static final int SUNDAY = 7; - - - public static void main(String[] args) { - int day = THURSDAY; - - switch (day) { - case MONDAY, FRIDAY, SUNDAY: System.out.println(" 6"); break; - case TUESDAY : System.out.println(" 7"); break; - case THURSDAY, SATURDAY : System.out.println(" 8"); break; - case WEDNESDAY : System.out.println(" 9"); break; - } - } -} diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java12/SwitchExpressions.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java12/SwitchExpressions.java deleted file mode 100644 index 86ff120576..0000000000 --- a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java12/SwitchExpressions.java +++ /dev/null @@ -1,35 +0,0 @@ -/** - * - * @see JEP 325: Switch Expressions (Preview) - */ -public class SwitchExpressions { - private static final int MONDAY = 1; - private static final int TUESDAY = 2; - private static final int WEDNESDAY = 3; - private static final int THURSDAY = 4; - private static final int FRIDAY = 5; - private static final int SATURDAY = 6; - private static final int SUNDAY = 7; - - - public static void main(String[] args) { - int day = FRIDAY; - - var numLetters = switch (day) { - case MONDAY, FRIDAY, SUNDAY -> 6; - case TUESDAY -> 7; - case THURSDAY, SATURDAY -> 8; - case WEDNESDAY -> 9; - default -> { - int k = day * 2; - int result = f(k); - break result; - } - }; - System.out.printf("NumLetters: %d%n", numLetters); - } - - private static int f(int k) { - return k*3; - } -} diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java12/SwitchExpressionsBreak.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java12/SwitchExpressionsBreak.java deleted file mode 100644 index 6584e4f5df..0000000000 --- a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java12/SwitchExpressionsBreak.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * - * @see JEP 325: Switch Expressions (Preview) - */ -public class SwitchExpressionsBreak { - private static final int MONDAY = 1; - private static final int TUESDAY = 2; - private static final int WEDNESDAY = 3; - private static final int THURSDAY = 4; - private static final int FRIDAY = 5; - private static final int SATURDAY = 6; - private static final int SUNDAY = 7; - - private static final int SIX = 6; - - public static void main(String[] args) { - int day = FRIDAY; - - var numLetters = switch (day) { - case MONDAY, FRIDAY, SUNDAY: break SwitchExpressionsBreak.SIX; - case TUESDAY : break 7; - case THURSDAY, SATURDAY : break 8; - case WEDNESDAY : break 9; - default : { - int k = day * 2; - int result = f(k); - break result; - } - }; - System.out.printf("NumLetters: %d%n", numLetters); - } - - private static int f(int k) { - return k*3; - } -} diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java12/SwitchRules.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java12/SwitchRules.java deleted file mode 100644 index 844abf6bbb..0000000000 --- a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java12/SwitchRules.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * - * @see JEP 325: Switch Expressions (Preview) - */ -public class SwitchRules { - private static final int MONDAY = 1; - private static final int TUESDAY = 2; - private static final int WEDNESDAY = 3; - private static final int THURSDAY = 4; - private static final int FRIDAY = 5; - private static final int SATURDAY = 6; - private static final int SUNDAY = 7; - - public static void main(String[] args) { - int day = WEDNESDAY; - - switch (day) { - case MONDAY, FRIDAY, SUNDAY -> System.out.println(" 6"); - case TUESDAY -> System.out.println(" 7"); - case THURSDAY, SATURDAY -> System.out.println(" 8"); - case WEDNESDAY -> { System.out.println(" 9"); } - default -> throw new IllegalArgumentException(); - } - } -} diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java13/TextBlocks.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java13/TextBlocks.java new file mode 100644 index 0000000000..78a6d111c3 --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java13/TextBlocks.java @@ -0,0 +1,55 @@ +/** + * @see JEP 355: Text Blocks (Preview) + */ +public class TextBlocks { + + public static void main(String[] args) { + String html = """ + + +

Hello, world

+ + + """; + + System.out.println(html); + + String season = """ + winter"""; // the six characters w i n t e r + + String period = """ + winter + """; // the seven characters w i n t e r LF + + String greeting = + """ + Hi, "Bob" + """; // the ten characters H i , SP " B o b " LF + + String salutation = + """ + Hi, + "Bob" + """; // the eleven characters H i , LF SP " B o b " LF + + String empty = """ + """; // the empty string (zero length) + + String quote = """ + " + """; // the two characters " LF + + String backslash = """ + \\ + """; // the two characters \ LF + + String normalStringLiteral = "test"; + + String code = + """ + String text = \""" + A text block inside a text block + \"""; + """; + } +} diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java14/Point.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java14/Point.java new file mode 100644 index 0000000000..ac3fe4fd46 --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java14/Point.java @@ -0,0 +1,10 @@ +/** + * @see JEP 359: Records (Preview) + */ +public record Point(int x, int y) { + + public static void main(String[] args) { + Point p = new Point(1, 2); + System.out.println("p = " + p); + } +} diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java14/Records.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java14/Records.java new file mode 100644 index 0000000000..2368b2ae7c --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java14/Records.java @@ -0,0 +1,61 @@ +import java.io.IOException; +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; + +/** + * @see JEP 359: Records (Preview) + */ +public class Records { + + @Target(ElementType.TYPE_USE) + @interface Nullable { } + + @Target({ElementType.CONSTRUCTOR, ElementType.PARAMETER}) + @interface MyAnnotation { } + + public record MyComplex(int real, @Deprecated int imaginary) { + // explicit declaration of a canonical constructor + @MyAnnotation + public MyComplex(@MyAnnotation int real, int imaginary) { + if (real > 100) throw new IllegalArgumentException("too big"); + this.real = real; + this.imaginary = imaginary; + } + public record Nested(int a) {} + } + + + public record Range(int lo, int hi) { + // compact record constructor + @MyAnnotation + public Range { + if (lo > hi) /* referring here to the implicit constructor parameters */ + throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi)); + } + + public void foo() { } + } + + public record VarRec(@Nullable @Deprecated String @Nullable ... x) {} + + public record ArrayRec(int x[]) {} + + public record EmptyRec() { + public void foo() { } + public Type bar() { return null; } + public static void baz() { + EmptyRec r = new EmptyRec<>(); + System.out.println(r); + } + } + + // see https://www.javaspecialists.eu/archive/Issue276.html + public interface Person { + String firstName(); + String lastName(); + } + public record PersonRecord(String firstName, String lastName) + implements Person, java.io.Serializable { + + } +} diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java14/TextBlocks.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java14/TextBlocks.java new file mode 100644 index 0000000000..5ff553185f --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java14/TextBlocks.java @@ -0,0 +1,113 @@ +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; + +/** + * @see JEP 368: Text Blocks (Second Preview) + */ +public class TextBlocks { + + + public static void main(String[] args) throws Exception { + // note: there is trailing whitespace!! + String html = """ + + +

Hello, world

+ + + """; + System.out.println(html); + + String query = """ + SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB` + WHERE `CITY` = 'INDIANAPOLIS' + ORDER BY `EMP_ID`, `LAST_NAME`; + """; + System.out.println(query); + + ScriptEngine engine = new ScriptEngineManager().getEngineByName("js"); + Object obj = engine.eval(""" + function hello() { + print('"Hello, world"'); + } + + hello(); + """); + + // Escape sequences + String htmlWithEscapes = """ + \r + \r +

Hello, world

\r + \r + \r + """; + System.out.println(htmlWithEscapes); + + String season = """ + winter"""; // the six characters w i n t e r + + String period = """ + winter + """; // the seven characters w i n t e r LF + + String greeting = + """ + Hi, "Bob" + """; // the ten characters H i , SP " B o b " LF + + String salutation = + """ + Hi, + "Bob" + """; // the eleven characters H i , LF SP " B o b " LF + + String empty = """ + """; // the empty string (zero length) + + String quote = """ + " + """; // the two characters " LF + + String backslash = """ + \\ + """; // the two characters \ LF + + String normalStringLiteral = "test"; + + String code = + """ + String text = \""" + A text block inside a text block + \"""; + """; + + // new escape sequences + String text = """ + Lorem ipsum dolor sit amet, consectetur adipiscing \ + elit, sed do eiusmod tempor incididunt ut labore \ + et dolore magna aliqua.\ + """; + System.out.println(text); + + String colors = """ + red \s + green\s + blue \s + """; + System.out.println(colors); + + // empty new line as first content + String emptyLine = """ + +test +"""; + System.out.println(emptyLine.replaceAll("\n", "")); + + // backslash escapes + String bs = """ + \\test + """; + System.out.println(bs.replaceAll("\n", "")); + } +} diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/design/xml/UselessOverridingMethod.xml b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/design/xml/UselessOverridingMethod.xml index b242777cda..dc0889ee2e 100644 --- a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/design/xml/UselessOverridingMethod.xml +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/design/xml/UselessOverridingMethod.xml @@ -3,10 +3,9 @@ xmlns="http://pmd.sourceforge.net/rule-tests" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pmd.sourceforge.net/rule-tests http://pmd.sourceforge.net/rule-tests_1_0_0.xsd"> + - + call super 1 + - + call super with same argument 1 + - + call super with different argument 0 + - + call super with different argument 2 0 + - + call super with different argument 3 0 + - + call super with inverted arguments 0 + - + return value of super 1 + - + return value of super with argument 1 + - + return value of super after adding a string 0 + - + do not crash on abstract methods 0 + - + do not crash on interfaces 0 + - + do not crash on empty returns 0 + - + do not crash on super 0 + - + call super with different argument 4 0 + - + adding final is OK 0 + - + adding synchronized is OK, see sf-bug #423 0 + - + Constructors are OK 0 + - + Should ignore clone implementation ( see bug 1522517) 0 + - + clone method with arguments should not be ignored 1 - - - - + + + False +: Overriding method merely calls super (see bug 1415525) 0 - - Method signature in super class: protected void writeChild(nu.xom.Node arg0) throws java.io.IOException; + * + *

See: https://sourceforge.net/tracker/?func=detail&aid=1415525&group_id=56262&atid=479921 + */ + public void writeChild(Node node) throws IOException { + super.writeChild(node); + } } ]]> + - - - + [ 1977230 ] false positive: UselessOverridingMethod 0 - - - - - + + + [ 2142986 ] UselessOverridingMethod doesn't consider annotations, ignoreAnnotations property set to true true 1 - - - - - + + + [ 2142986 ] UselessOverridingMethod doesn't consider annotations 0 - - - - - + + + [ 2142986 ] UselessOverridingMethod doesn't consider annotations, @Override only 1 - + - + ClassCastException in statement cast 0 comparator = new Comparator() { + public void method() { + @SuppressWarnings("unused") + final Comparator comparator = new Comparator() { - @Override - public int compare(@Nonnull Long o1, @Nonnull Long o2) { - return 0; - } - }; - } + @Override + public int compare(@Nonnull Long o1, @Nonnull Long o2) { + return 0; + } + }; + } } ]]> + + + [java] UselessOverridingMethod false positive when elevating access modifier #911 - direct + 0 + + + + + [java] UselessOverridingMethod false positive when elevating access modifier #911 - direct but changing arguments + 0 + + + + + [java] UselessOverridingMethod false positive when elevating access modifier #911 - direct different package, same visibility + 0 + + + + + [java] UselessOverridingMethod false positive when elevating access modifier #911 - transitive + 0 + + + + + [java] UselessOverridingMethod false positive when elevating access modifier #911 - direct2 + 1 + 5 + + + + + Make sure, overriding to add synchronized is Ok, see sf-bug #423 + 0 + + \ No newline at end of file diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/errorprone/xml/MisplacedNullCheck.xml b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/errorprone/xml/MisplacedNullCheck.xml index 5291a2822a..1d23d6f199 100644 --- a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/errorprone/xml/MisplacedNullCheck.xml +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/errorprone/xml/MisplacedNullCheck.xml @@ -3,36 +3,61 @@ xmlns="http://pmd.sourceforge.net/rule-tests" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pmd.sourceforge.net/rule-tests http://pmd.sourceforge.net/rule-tests_1_0_0.xsd"> + - - 1 + null check after method invocation with conditional AND and != + 4 + 3,4,6,7 + + The null check here is misplaced; if the variable 'a' is null there will be a NullPointerException + The null check here is misplaced; if the variable 'a' is null there will be a NullPointerException + The null check here is misplaced; if the variable 'a' is null there will be a NullPointerException + The null check here is misplaced; if the variable 'a' is null there will be a NullPointerException + + - - 1 + null check after nested method invocation + 4 + 3,4,6,7 + + The null check here is misplaced; if the variable 'baz' is null there will be a NullPointerException + The null check here is misplaced; if the variable 'baz' is null there will be a NullPointerException + The null check here is misplaced; if the variable 'baz' is null there will be a NullPointerException + The null check here is misplaced; if the variable 'baz' is null there will be a NullPointerException + + - + null check before nested method invocation 0 + - + 1610730: null check after method invocation with conditional OR and == 1 + 3 + + The null check here is misplaced; if the variable 'a' is null there will be a NullPointerException + + - + 3372128: False positive: ArrayIsStoredDirectly 0 + #977 MisplacedNullCheck makes false positives 0 @@ -81,8 +109,68 @@ public class Test { if ((value != null && !value.equals(oldValue)) || value == null) { // Do something } + + if ((value == null || !value.equals(oldValue)) && value != null) {} } } ]]> + + + #2242 False-positive MisplacedNullCheck reported (1) + 0 + + + + + #2242 False-positive MisplacedNullCheck reported (2) + 0 + + + + + False-positive/negative with multiple conditions + 2 + 7,8 + + The null check here is misplaced; if the variable 'attributes' is null there will be a NullPointerException + The null check here is misplaced; if the variable 'attributes' is null there will be a NullPointerException + + attributes) { + boolean isStereotype = annotationType.equals("javax.inject.Named"); + if (isStereotype && attributes != null && attributes.containsKey("value")) {} + if (isStereotype || attributes == null || attributes.containsKey("value")) {} + + if (isStereotype && attributes.containsKey("value") && attributes != null) {} + if (isStereotype || attributes.containsKey("value") || attributes == null) {} + } +} + ]]> + diff --git a/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseParsingHelper.kt b/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseParsingHelper.kt index 651d2988d6..2f7418ab28 100644 --- a/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseParsingHelper.kt +++ b/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseParsingHelper.kt @@ -51,7 +51,8 @@ abstract class BaseParsingHelper, T : RootNode */ fun getVersion(version: String?): LanguageVersion { val language = LanguageRegistry.getLanguage(langName) - return if (version == null) language.defaultVersion else language.getVersion(version) + return if (version == null) language.defaultVersion + else language.getVersion(version) ?: throw AssertionError("Unsupported version $version for language $language") } val defaultVersion: LanguageVersion diff --git a/pom.xml b/pom.xml index 10bd52d9e3..5812d7ef4e 100644 --- a/pom.xml +++ b/pom.xml @@ -629,7 +629,7 @@ org.ow2.asm asm - 7.1 + 7.3.1 net.sourceforge.pmd