diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md index 8abb75f161..68adf5716b 100644 --- a/docs/pages/release_notes.md +++ b/docs/pages/release_notes.md @@ -21,6 +21,10 @@ This is a {{ site.pmd.release_type }} release. ### Fixed Issues +* java + * [#3117](https://github.com/pmd/pmd/issues/3117): \[java] Infinite loop when parsing invalid code nested in lambdas + * [#3145](https://github.com/pmd/pmd/issues/3145): \[java] Parse exception when using "record" as variable name + ### API Changes ### External Contributions diff --git a/pmd-java/etc/grammar/Java.jjt b/pmd-java/etc/grammar/Java.jjt index 0dc3919471..ba6269daba 100644 --- a/pmd-java/etc/grammar/Java.jjt +++ b/pmd-java/etc/grammar/Java.jjt @@ -1,4 +1,10 @@ /** + * Fix #3117 - infinite loop when parsing invalid code nested in lambdas + * Andreas Dangel 03/2021 + *==================================================================== + * Fix #3145 - parse exception with local records + * Clément Fournier 03/2021 + *==================================================================== * Remove support for Java 14 preview language features * JEP 397: Sealed Classes (Second Preview) for Java16 Preview * JEP 395: Records for Java16 @@ -232,6 +238,11 @@ options { VISITOR = true; NODE_PACKAGE="net.sourceforge.pmd.lang.java.ast"; + // disable the calculation of expected tokens when a parse error occurs + // depending on the possible allowed next tokens, this + // could be expensive (see https://github.com/pmd/pmd/issues/3117) + //ERROR_REPORTING = false; + //DEBUG_PARSER = true; //DEBUG_LOOKAHEAD = true; //DEBUG_TOKEN_MANAGER = true; @@ -312,7 +323,7 @@ class JavaParserImpl { } private boolean isRecordStart() { - return isRecordTypeSupported() && isKeyword("record"); + return isRecordTypeSupported() && isKeyword("record") && isToken(2, IDENTIFIER); } private boolean isEnumStart() { @@ -381,8 +392,7 @@ class JavaParserImpl { next.kind == INTERFACE || next.kind == AT && isToken(2, INTERFACE) || next.kind == IDENTIFIER && next.getImage().equals("enum") - || - next.kind == IDENTIFIER && next.image.equals("record") + || next.kind == IDENTIFIER && next.getImage().equals("record") && isToken(2, IDENTIFIER) ); } diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/ParserCornersTest.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/ParserCornersTest.java index c614666367..2d4e69824a 100644 --- a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/ParserCornersTest.java +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/ParserCornersTest.java @@ -186,13 +186,22 @@ public class ParserCornersTest extends BaseJavaTreeDumpTest { doTest("GitHubBug309", java8); } - @Test + @Test(timeout = 30000) public void testInfiniteLoopInLookahead() { - expect.expect(ParseException.class); // the code is invalid. The test fails if it times out + expect.expect(ParseException.class); // https://github.com/pmd/pmd/issues/3117 java8.parseResource("InfiniteLoopInLookahead.java"); } + @Test + public void stringConcatentationShouldNotBeCast() { + // https://github.com/pmd/pmd/issues/1484 + String code = "public class Test {\n" + " public static void main(String[] args) {\n" + + " System.out.println(\"X\" + (args) + \"Y\");\n" + " }\n" + "}"; + Assert.assertEquals(0, java8.parse(code).findDescendantsOfType(ASTCastExpression.class).size()); + } + + /** * Empty statements should be allowed. * diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/InfiniteLoopInLookahead.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/InfiniteLoopInLookahead.java index bdbd367cdf..55552bd48a 100644 --- a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/InfiniteLoopInLookahead.java +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/InfiniteLoopInLookahead.java @@ -15,7 +15,7 @@ public class InfiniteLoopInLookahead { resList.forEach(f -> { resList.forEach(g -> { resList.forEach(h -> { - resList + resList // note: missing semicolon -> parse error here... }); }); }); diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/LocalRecords.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/LocalRecords.java index c359b99419..b860e99136 100644 --- a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/LocalRecords.java +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/LocalRecords.java @@ -31,6 +31,12 @@ public class LocalRecords { final @Deprecated static record MyRecord4(String a) {} } + void statementThatStartsWithRecordAsRegularIdent() { + // https://github.com/pmd/pmd/issues/3145 + final Map record = new HashMap<>(); + record.put("key", "value"); + } + void methodWithLocalClass() { class MyLocalClass {} } diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/LocalRecords.txt b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/LocalRecords.txt index 49926889d3..3f722122aa 100644 --- a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/LocalRecords.txt +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/LocalRecords.txt @@ -3,7 +3,7 @@ +- ImportDeclaration[@ImportOnDemand = "false", @ImportedName = "java.util.List", @ImportedSimpleName = "List", @PackageName = "java.util", @Static = "false"] +- ClassOrInterfaceDeclaration[@Abstract = "false", @Annotation = "false", @Anonymous = "false", @BinaryName = "LocalRecords", @CanonicalName = "LocalRecords", @EffectiveVisibility = "public", @Enum = "false", @Final = "false", @Interface = "false", @Local = "false", @Nested = "false", @PackageName = "", @PackagePrivate = "false", @Record = "false", @RegularClass = "true", @SimpleName = "LocalRecords", @TopLevel = "true", @Visibility = "public"] +- ModifierList[@EffectiveModifiers = "{public}", @ExplicitModifiers = "{public}"] - +- ClassOrInterfaceBody[@Size = "6"] + +- ClassOrInterfaceBody[@Size = "7"] +- ClassOrInterfaceDeclaration[@Abstract = "true", @Annotation = "false", @Anonymous = "false", @BinaryName = "LocalRecords$Merchant", @CanonicalName = "LocalRecords.Merchant", @EffectiveVisibility = "public", @Enum = "false", @Final = "false", @Interface = "true", @Local = "false", @Nested = "true", @PackageName = "", @PackagePrivate = "false", @Record = "false", @RegularClass = "false", @SimpleName = "Merchant", @TopLevel = "false", @Visibility = "public"] | +- ModifierList[@EffectiveModifiers = "{public, abstract, static}", @ExplicitModifiers = "{public}"] | +- ClassOrInterfaceBody[@Size = "0"] @@ -147,6 +147,29 @@ | | +- ClassOrInterfaceType[@FullyQualified = "false", @SimpleName = "String"] | | +- VariableDeclaratorId[@ArrayType = "false", @EffectiveVisibility = "local", @EnumConstant = "false", @ExceptionBlockParameter = "false", @Field = "false", @Final = "true", @FormalParameter = "false", @LambdaParameter = "false", @LocalVariable = "false", @Name = "a", @PatternBinding = "false", @RecordComponent = "true", @ResourceDeclaration = "false", @TypeInferred = "false", @Visibility = "private"] | +- RecordBody[@Size = "0"] + +- MethodDeclaration[@Abstract = "false", @Arity = "0", @EffectiveVisibility = "package", @Image = "statementThatStartsWithRecordAsRegularIdent", @Name = "statementThatStartsWithRecordAsRegularIdent", @Overridden = "false", @Varargs = "false", @Visibility = "package", @Void = "true"] + | +- ModifierList[@EffectiveModifiers = "{}", @ExplicitModifiers = "{}"] + | +- VoidType[] + | +- FormalParameters[@Size = "0"] + | +- Block[@Size = "2", @containsComment = "false"] + | +- LocalVariableDeclaration[@EffectiveVisibility = "local", @Final = "true", @TypeInferred = "false", @Visibility = "local"] + | | +- ModifierList[@EffectiveModifiers = "{final}", @ExplicitModifiers = "{final}"] + | | +- ClassOrInterfaceType[@FullyQualified = "false", @SimpleName = "Map"] + | | | +- TypeArguments[@Diamond = "false", @Size = "2"] + | | | +- ClassOrInterfaceType[@FullyQualified = "false", @SimpleName = "String"] + | | | +- ClassOrInterfaceType[@FullyQualified = "false", @SimpleName = "String"] + | | +- VariableDeclarator[@Initializer = "true", @Name = "record"] + | | +- VariableDeclaratorId[@ArrayType = "false", @EffectiveVisibility = "local", @EnumConstant = "false", @ExceptionBlockParameter = "false", @Field = "false", @Final = "true", @FormalParameter = "false", @LambdaParameter = "false", @LocalVariable = "true", @Name = "record", @PatternBinding = "false", @RecordComponent = "false", @ResourceDeclaration = "false", @TypeInferred = "false", @Visibility = "local"] + | | +- ConstructorCall[@AnonymousClass = "false", @CompileTimeConstant = "false", @DiamondTypeArgs = "true", @MethodName = "new", @ParenthesisDepth = "0", @Parenthesized = "false", @QualifiedInstanceCreation = "false"] + | | +- ClassOrInterfaceType[@FullyQualified = "false", @SimpleName = "HashMap"] + | | | +- TypeArguments[@Diamond = "true", @Size = "0"] + | | +- ArgumentList[@Size = "0"] + | +- ExpressionStatement[] + | +- MethodCall[@CompileTimeConstant = "false", @Image = "put", @MethodName = "put", @ParenthesisDepth = "0", @Parenthesized = "false"] + | +- VariableAccess[@AccessType = "READ", @CompileTimeConstant = "false", @Image = "record", @Name = "record", @ParenthesisDepth = "0", @Parenthesized = "false"] + | +- ArgumentList[@Size = "2"] + | +- StringLiteral[@CompileTimeConstant = "true", @ConstValue = "key", @Empty = "false", @Image = "\"key\"", @Length = "3", @ParenthesisDepth = "0", @Parenthesized = "false", @TextBlock = "false"] + | +- StringLiteral[@CompileTimeConstant = "true", @ConstValue = "value", @Empty = "false", @Image = "\"value\"", @Length = "5", @ParenthesisDepth = "0", @Parenthesized = "false", @TextBlock = "false"] +- MethodDeclaration[@Abstract = "false", @Arity = "0", @EffectiveVisibility = "package", @Image = "methodWithLocalClass", @Name = "methodWithLocalClass", @Overridden = "false", @Varargs = "false", @Visibility = "package", @Void = "true"] | +- ModifierList[@EffectiveModifiers = "{}", @ExplicitModifiers = "{}"] | +- VoidType[]