Merge branch 'main' into pr-5303

This commit is contained in:
Andreas Dangel 2024-11-17 15:25:24 +01:00
commit 85aeebbab0
No known key found for this signature in database
GPG Key ID: 93450DF2DF9A3FA3
47 changed files with 1533 additions and 113 deletions

View File

@ -7866,6 +7866,33 @@
"contributions": [
"code"
]
},
{
"login": "chenguangqi",
"name": "天热吃西瓜",
"avatar_url": "https://avatars.githubusercontent.com/u/6231010?v=4",
"profile": "http://chenguangqi.github.io/",
"contributions": [
"bug"
]
},
{
"login": "wahajenius",
"name": "Willem A. Hajenius",
"avatar_url": "https://avatars.githubusercontent.com/u/7836322?v=4",
"profile": "https://github.com/wahajenius",
"contributions": [
"code"
]
},
{
"login": "VitaliiIevtushenko",
"name": "Vitalii Yevtushenko",
"avatar_url": "https://avatars.githubusercontent.com/u/11145125?v=4",
"profile": "https://github.com/VitaliiIevtushenko",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,

View File

@ -75,7 +75,7 @@ GEM
racc (1.8.1)
rchardet (1.8.0)
rexml (3.3.9)
rouge (4.4.0)
rouge (4.5.0)
rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1)
safe_yaml (1.0.5)

View File

@ -266,7 +266,7 @@ GEM
concurrent-ruby (~> 1.0)
unicode-display_width (1.8.0)
uri (0.13.1)
webrick (1.8.2)
webrick (1.9.0)
PLATFORMS
x86_64-linux

View File

@ -17,6 +17,21 @@ This is a {{ site.pmd.release_type }} release.
### 🐛 Fixed Issues
* ant
* [#1860](https://github.com/pmd/pmd/issues/1860): \[ant] Reflective access warnings on java > 9 and java < 17
* apex
* [#5333](https://github.com/pmd/pmd/issues/5333): \[apex] Token recognition errors for string containing unicode escape sequence
* html
* [#5322](https://github.com/pmd/pmd/issues/5322): \[html] CPD throws exception on when HTML file is missing closing tag
* java
* [#5293](https://github.com/pmd/pmd/issues/5293): \[java] Deadlock when executing PMD in multiple threads
* [#5324](https://github.com/pmd/pmd/issues/5324): \[java] Issue with type inference of nested lambdas
* [#5329](https://github.com/pmd/pmd/issues/5329): \[java] Type inference issue with unknown method ref in call chain
* java-bestpractices
* [#5083](https://github.com/pmd/pmd/issues/5083): \[java] UnusedPrivateMethod false positive when method reference has no target type
* [#5097](https://github.com/pmd/pmd/issues/5097): \[java] UnusedPrivateMethod FP with raw type missing from the classpath
* [#5318](https://github.com/pmd/pmd/issues/5318): \[java] PreserveStackTraceRule: false-positive on Pattern Matching with instanceof
* java-performance
* [#5287](https://github.com/pmd/pmd/issues/5287): \[java] TooFewBranchesForSwitch false-positive with switch using list of case constants
* [#5314](https://github.com/pmd/pmd/issues/5314): \[java] InsufficientStringBufferDeclarationRule: Lack of handling for char type parameters
### 🚨 API Changes
@ -26,6 +41,7 @@ This is a {{ site.pmd.release_type }} release.
instead (note different package `ast` instead of `antlr4`).
### ✨ External Contributions
* [#5284](https://github.com/pmd/pmd/pull/5284): \[apex] Use case-insensitive input stream to avoid choking on Unicode escape sequences - [Willem A. Hajenius](https://github.com/wahajenius) (@wahajenius)
{% endtocmaker %}

View File

@ -14,14 +14,19 @@ import java.util.List;
import java.util.Map;
import java.util.RandomAccess;
import org.antlr.v4.runtime.BaseErrorListener;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.Token;
import net.sourceforge.pmd.annotation.InternalApi;
import net.sourceforge.pmd.lang.ast.LexException;
import net.sourceforge.pmd.lang.document.TextDocument;
import net.sourceforge.pmd.lang.document.TextRegion;
import io.github.apexdevtools.apexparser.ApexLexer;
import io.github.apexdevtools.apexparser.CaseInsensitiveInputStream;
@InternalApi
final class ApexCommentBuilder {
@ -103,7 +108,15 @@ final class ApexCommentBuilder {
}
private static CommentInformation extractInformationFromComments(TextDocument sourceCode, String suppressMarker) {
ApexLexer lexer = new ApexLexer(CharStreams.fromString(sourceCode.getText().toString()));
String source = sourceCode.getText().toString();
ApexLexer lexer = new ApexLexer(new CaseInsensitiveInputStream(CharStreams.fromString(source)));
lexer.removeErrorListeners();
lexer.addErrorListener(new BaseErrorListener() {
@Override
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) {
throw new LexException(line, charPositionInLine, sourceCode.getFileId(), msg, e);
}
});
List<Token> allCommentTokens = new ArrayList<>();
Map<Integer, String> suppressMap = new HashMap<>();

View File

@ -66,4 +66,12 @@ class ApexCommentTest extends ApexParserTestBase {
ASTFormalComment comment = file.descendants(ASTUserClass.class).children(ASTFormalComment.class).first();
assertEquals(FORMAL_COMMENT_CONTENT, comment.getImage());
}
@Test
void fileWithUnicodeEscapes() {
ASTApexFile file = apex.parse(FORMAL_COMMENT_CONTENT + "\n"
+ "class MyClass { String s = 'Fran\\u00E7ois'; }");
ASTFormalComment comment = file.descendants(ASTUserClass.class).children(ASTFormalComment.class).first();
assertEquals(FORMAL_COMMENT_CONTENT, comment.getImage());
}
}

View File

@ -8,14 +8,18 @@ package net.sourceforge.pmd.lang.apex.ast;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.antlr.v4.runtime.BaseErrorListener;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.Token;
import org.junit.jupiter.api.Test;
import io.github.apexdevtools.apexparser.ApexLexer;
import io.github.apexdevtools.apexparser.ApexParser;
import io.github.apexdevtools.apexparser.CaseInsensitiveInputStream;
/**
* This is an exploration test for {@link ApexLexer}.
@ -49,4 +53,36 @@ class ApexLexerTest {
ApexParser.CompilationUnitContext compilationUnit = parser.compilationUnit();
assertNotNull(compilationUnit);
}
@Test
void testLexerUnicodeEscapes() {
String s = "'Fran\\u00E7ois'";
// note: with apex-parser 4.3.1, no errors are reported anymore
assertEquals(2, getLexingErrors(CharStreams.fromString(s)));
assertEquals(0, getLexingErrors(new CaseInsensitiveInputStream(CharStreams.fromString(s))));
}
private int getLexingErrors(CharStream stream) {
ApexLexer lexer = new ApexLexer(stream);
ErrorListener errorListener = new ErrorListener();
lexer.removeErrorListeners(); // Avoid distracting "token recognition error" stderr output
lexer.addErrorListener(errorListener);
CommonTokenStream tokens = new CommonTokenStream(lexer);
tokens.fill();
return errorListener.getErrorCount();
}
private static class ErrorListener extends BaseErrorListener {
private int errorCount = 0;
@Override
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line,
int charPositionInLine, String msg, RecognitionException e) {
++errorCount;
}
public int getErrorCount() {
return errorCount;
}
}
}

View File

@ -178,7 +178,7 @@
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.26.0</version>
<version>1.27.1</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@ -50,15 +50,14 @@ class LineNumbers {
nextIndex = determineLocation((AbstractHtmlNode<?>) child, nextIndex);
}
// autoclosing element, eg <a />
boolean isAutoClose = n.getNumChildren() == 0
&& n instanceof ASTHtmlElement
// nextIndex is up to the closing > at this point
&& htmlString.startsWith("/>", nextIndex - 2);
// explicitly closing element, eg. </a>
boolean hasCloseElement = n instanceof ASTHtmlElement
// nextIndex is up to the closing tag at this point
&& htmlString.startsWith("</" + n.getXPathNodeName() + ">", nextIndex);
if (n instanceof ASTHtmlDocument) {
nextIndex = htmlString.length();
} else if (n instanceof ASTHtmlElement && !isAutoClose) {
} else if (n instanceof ASTHtmlElement && hasCloseElement) {
nextIndex += 2 + n.getXPathNodeName().length() + 1; // </nodename>
} else if (n instanceof ASTHtmlComment) {
nextIndex += 4 + 3; // <!-- and -->

View File

@ -21,4 +21,13 @@ class HtmlCpdLexerTest extends CpdTextComparisonTest {
doTest("SimpleHtmlFile");
}
@Test
void invalidHtml() {
doTest("InvalidHtml");
}
@Test
void metaTag() {
doTest("MetaTag");
}
}

View File

@ -0,0 +1,7 @@
<!doctype html>
<html lang="en">
<body>
<!-- missing closing tag for div -->
<div class='wrapper'>
</body>
</html>

View File

@ -0,0 +1,22 @@
[Image] or [Truncated image[ Bcol Ecol
L1
[#document] 1 8
[#doctype] 1 15
[\n] 16 16
L2
[html] 1 7
[\n] 17 17
L3
[body] 1 7
[\n] 7 7
L4
[#comment] 1 36
[\n] 37 37
L5
[div] 1 22
[\n] 22 22
L6
[\n] 8 8
L7
[\n] 8 8
EOF

View File

@ -0,0 +1,9 @@
<!doctype html>
<html lang="en">
<head>
<!-- meta tag doesn't have a closing tag and this is valid -->
<meta charset="UTF-8">
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,27 @@
[Image] or [Truncated image[ Bcol Ecol
L1
[#document] 1 8
[#doctype] 1 15
[\n] 16 16
L2
[html] 1 7
[\n] 17 17
L3
[head] 1 7
[\n ] 7 4
L4
[#comment] 5 66
[\n ] 67 4
L5
[meta] 5 27
[\n] 27 27
L6
[\n] 8 8
L7
[body] 1 7
[\n] 7 7
L8
[\n] 8 8
L9
[\n] 8 8
EOF

View File

@ -4,11 +4,10 @@
package net.sourceforge.pmd.lang.java.rule.bestpractices;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.NonNull;
import net.sourceforge.pmd.lang.ast.NodeStream;
import net.sourceforge.pmd.lang.java.ast.ASTAssignableExpr.ASTNamedReferenceExpr;
import net.sourceforge.pmd.lang.java.ast.ASTCastExpression;
@ -17,13 +16,17 @@ import net.sourceforge.pmd.lang.java.ast.ASTConditionalExpression;
import net.sourceforge.pmd.lang.java.ast.ASTConstructorCall;
import net.sourceforge.pmd.lang.java.ast.ASTExpression;
import net.sourceforge.pmd.lang.java.ast.ASTFieldDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTInfixExpression;
import net.sourceforge.pmd.lang.java.ast.ASTInitializer;
import net.sourceforge.pmd.lang.java.ast.ASTList;
import net.sourceforge.pmd.lang.java.ast.ASTMethodCall;
import net.sourceforge.pmd.lang.java.ast.ASTPatternExpression;
import net.sourceforge.pmd.lang.java.ast.ASTThrowStatement;
import net.sourceforge.pmd.lang.java.ast.ASTTypeDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTTypePattern;
import net.sourceforge.pmd.lang.java.ast.ASTVariableAccess;
import net.sourceforge.pmd.lang.java.ast.ASTVariableId;
import net.sourceforge.pmd.lang.java.ast.BinaryOp;
import net.sourceforge.pmd.lang.java.ast.InvocationNode;
import net.sourceforge.pmd.lang.java.ast.JavaNode;
import net.sourceforge.pmd.lang.java.ast.internal.JavaAstUtils;
@ -64,7 +67,7 @@ public class PreserveStackTraceRule extends AbstractJavaRulechainRule {
for (ASTThrowStatement throwStatement : catchStmt.getBody().descendants(ASTThrowStatement.class)) {
ASTExpression thrownExpr = throwStatement.getExpr();
if (!exprConsumesException(exceptionParam, thrownExpr, true)) {
if (!exprConsumesException(Collections.singleton(exceptionParam), thrownExpr, true)) {
asCtx(data).addViolation(thrownExpr, exceptionParam.getName());
}
}
@ -72,25 +75,39 @@ public class PreserveStackTraceRule extends AbstractJavaRulechainRule {
return null;
}
private boolean exprConsumesException(ASTVariableId exceptionParam, ASTExpression expr, boolean mayBeSelf) {
private boolean exprConsumesException(Set<ASTVariableId> exceptionParams, ASTExpression expr, boolean mayBeSelf) {
if (expr instanceof ASTConstructorCall) {
// new Exception(e)
return ctorConsumesException(exceptionParam, (ASTConstructorCall) expr);
return ctorConsumesException(exceptionParams, (ASTConstructorCall) expr);
} else if (expr instanceof ASTMethodCall) {
return methodConsumesException(exceptionParam, (ASTMethodCall) expr);
return methodConsumesException(exceptionParams, (ASTMethodCall) expr);
} else if (expr instanceof ASTCastExpression) {
ASTExpression innermost = JavaAstUtils.peelCasts(expr);
return exprConsumesException(exceptionParam, innermost, mayBeSelf);
return exprConsumesException(exceptionParams, innermost, mayBeSelf);
} else if (expr instanceof ASTConditionalExpression) {
ASTConditionalExpression ternary = (ASTConditionalExpression) expr;
return exprConsumesException(exceptionParam, ternary.getThenBranch(), mayBeSelf)
&& exprConsumesException(exceptionParam, ternary.getElseBranch(), mayBeSelf);
Set<ASTVariableId> possibleExceptionParams = new HashSet<>(exceptionParams);
// Peel out a type pattern variable in case this conditional is an instanceof pattern
NodeStream.of(ternary.getCondition())
.filterIs(ASTInfixExpression.class)
.filterMatching(ASTInfixExpression::getOperator, BinaryOp.INSTANCEOF)
.map(ASTInfixExpression::getRightOperand)
.filterIs(ASTPatternExpression.class)
.map(ASTPatternExpression::getPattern)
.filterIs(ASTTypePattern.class)
.map(ASTTypePattern::getVarId)
.firstOpt()
.ifPresent(possibleExceptionParams::add);
return exprConsumesException(possibleExceptionParams, ternary.getThenBranch(), mayBeSelf)
&& exprConsumesException(possibleExceptionParams, ternary.getElseBranch(), mayBeSelf);
} else if (expr instanceof ASTVariableAccess) {
JVariableSymbol referencedSym = ((ASTVariableAccess) expr).getReferencedSym();
@ -99,7 +116,7 @@ public class PreserveStackTraceRule extends AbstractJavaRulechainRule {
}
ASTVariableId decl = referencedSym.tryGetNode();
if (decl == exceptionParam) {
if (exceptionParams.contains(decl)) {
return mayBeSelf;
} else if (decl == null || decl.isFormalParameter() || decl.isField()) {
return false;
@ -113,16 +130,16 @@ public class PreserveStackTraceRule extends AbstractJavaRulechainRule {
// if any of the initializer and usages consumes the variable,
// answer true.
if (exprConsumesException(exceptionParam, decl.getInitializer(), mayBeSelf)) {
if (exprConsumesException(exceptionParams, decl.getInitializer(), mayBeSelf)) {
return true;
}
for (ASTNamedReferenceExpr usage : decl.getLocalUsages()) {
if (assignmentRhsConsumesException(exceptionParam, decl, usage)) {
if (assignmentRhsConsumesException(exceptionParams, decl, usage)) {
return true;
}
if (JavaAstUtils.followingCallChain(usage).any(it -> consumesExceptionNonRecursive(exceptionParam, it))) {
if (JavaAstUtils.followingCallChain(usage).any(it -> consumesExceptionNonRecursive(exceptionParams, it))) {
return true;
}
}
@ -134,7 +151,7 @@ public class PreserveStackTraceRule extends AbstractJavaRulechainRule {
}
}
private boolean assignmentRhsConsumesException(ASTVariableId exceptionParam, ASTVariableId lhsVariable, ASTNamedReferenceExpr usage) {
private boolean assignmentRhsConsumesException(Set<ASTVariableId> exceptionParams, ASTVariableId lhsVariable, ASTNamedReferenceExpr usage) {
if (usage.getIndexInParent() == 0) {
ASTExpression assignmentRhs = JavaAstUtils.getOtherOperandIfInAssignmentExpr(usage);
boolean rhsIsSelfReferential =
@ -142,25 +159,25 @@ public class PreserveStackTraceRule extends AbstractJavaRulechainRule {
.descendantsOrSelf()
.filterIs(ASTVariableAccess.class)
.any(it -> JavaAstUtils.isReferenceToVar(it, lhsVariable.getSymbol()));
return !rhsIsSelfReferential && exprConsumesException(exceptionParam, assignmentRhs, true);
return !rhsIsSelfReferential && exprConsumesException(exceptionParams, assignmentRhs, true);
}
return false;
}
private boolean ctorConsumesException(ASTVariableId exceptionParam, ASTConstructorCall ctorCall) {
return ctorCall.isAnonymousClass() && callsInitCauseInAnonInitializer(exceptionParam, ctorCall)
|| anArgumentConsumesException(exceptionParam, ctorCall);
private boolean ctorConsumesException(Set<ASTVariableId> exceptionParams, ASTConstructorCall ctorCall) {
return ctorCall.isAnonymousClass() && callsInitCauseInAnonInitializer(exceptionParams, ctorCall)
|| anArgumentConsumesException(exceptionParams, ctorCall);
}
private boolean consumesExceptionNonRecursive(ASTVariableId exceptionParam, ASTExpression expr) {
private boolean consumesExceptionNonRecursive(Set<ASTVariableId> exceptionParam, ASTExpression expr) {
if (expr instanceof ASTConstructorCall) {
return ctorConsumesException(exceptionParam, (ASTConstructorCall) expr);
}
return expr instanceof InvocationNode && anArgumentConsumesException(exceptionParam, (InvocationNode) expr);
}
private boolean methodConsumesException(ASTVariableId exceptionParam, ASTMethodCall call) {
if (anArgumentConsumesException(exceptionParam, call)) {
private boolean methodConsumesException(Set<ASTVariableId> exceptionParams, ASTMethodCall call) {
if (anArgumentConsumesException(exceptionParams, call)) {
return true;
}
ASTExpression qualifier = call.getQualifier();
@ -168,24 +185,24 @@ public class PreserveStackTraceRule extends AbstractJavaRulechainRule {
return false;
}
boolean mayBeSelf = ALLOWED_GETTERS.anyMatch(call);
return exprConsumesException(exceptionParam, qualifier, mayBeSelf);
return exprConsumesException(exceptionParams, qualifier, mayBeSelf);
}
private boolean callsInitCauseInAnonInitializer(ASTVariableId exceptionParam, ASTConstructorCall ctorCall) {
private boolean callsInitCauseInAnonInitializer(Set<ASTVariableId> exceptionParams, ASTConstructorCall ctorCall) {
return NodeStream.of(ctorCall.getAnonymousClassDeclaration())
.flatMap(ASTTypeDeclaration::getDeclarations)
.map(NodeStream.asInstanceOf(ASTFieldDeclaration.class, ASTInitializer.class))
.descendants().filterIs(ASTMethodCall.class)
.any(it -> isInitCauseWithTargetInArg(exceptionParam, it));
.any(it -> isInitCauseWithTargetInArg(exceptionParams, it));
}
private boolean isInitCauseWithTargetInArg(ASTVariableId exceptionSym, JavaNode expr) {
return INIT_CAUSE.matchesCall(expr) && anArgumentConsumesException(exceptionSym, (ASTMethodCall) expr);
private boolean isInitCauseWithTargetInArg(Set<ASTVariableId> exceptionParams, JavaNode expr) {
return INIT_CAUSE.matchesCall(expr) && anArgumentConsumesException(exceptionParams, (ASTMethodCall) expr);
}
private boolean anArgumentConsumesException(@NonNull ASTVariableId exceptionParam, InvocationNode thrownExpr) {
private boolean anArgumentConsumesException(Set<ASTVariableId> exceptionParams, InvocationNode thrownExpr) {
for (ASTExpression arg : ASTList.orEmptyStream(thrownExpr.getArguments())) {
if (exprConsumesException(exceptionParam, arg, true)) {
if (exprConsumesException(exceptionParams, arg, true)) {
return true;
}
}

View File

@ -240,6 +240,12 @@ public class InsufficientStringBufferDeclarationRule extends AbstractJavaRulecha
private int calculateExpression(ASTExpression expression) {
Object value = expression.getConstValue();
return value == null ? State.UNKNOWN_CAPACITY : (Integer) value;
if (value == null) {
return State.UNKNOWN_CAPACITY;
}
if (value instanceof Character) {
return (Character) value;
}
return (Integer) value;
}
}

View File

@ -84,12 +84,7 @@ final class ClassStub implements JClassSymbol, AsmStub, AnnotationOwner {
this.resolver = resolver;
this.names = new Names(internalName);
this.parseLock = new ParseLock() {
// note to devs: to debug the parsing logic you might have
// to replace the implementation of toString temporarily,
// otherwise an IDE could call toString just to show the item
// in the debugger view (which could cause parsing of the class file).
this.parseLock = new ParseLock("ClassStub:" + internalName) {
@Override
protected boolean doParse() throws IOException {
try (InputStream instream = loader.getInputStream()) {
@ -315,9 +310,9 @@ final class ClassStub implements JClassSymbol, AsmStub, AnnotationOwner {
}
@Override
public boolean isGeneric() {
public int getTypeParameterCount() {
parseLock.ensureParsed();
return signature.isGeneric();
return signature.getTypeParameterCount();
}
@Override

View File

@ -46,9 +46,9 @@ abstract class GenericSigBase<T extends JTypeParameterOwnerSymbol & AsmStub> {
protected List<JTypeVar> typeParameters;
private final ParseLock lock;
protected GenericSigBase(T ctx) {
protected GenericSigBase(T ctx, String parseLockName) {
this.ctx = ctx;
this.lock = new ParseLock() {
this.lock = new ParseLock(parseLockName) {
@Override
protected boolean doParse() {
GenericSigBase.this.doParse();
@ -81,7 +81,11 @@ abstract class GenericSigBase<T extends JTypeParameterOwnerSymbol & AsmStub> {
protected abstract boolean postCondition();
protected abstract boolean isGeneric();
protected abstract int getTypeParameterCount();
protected boolean isGeneric() {
return getTypeParameterCount() > 0;
}
public void setTypeParams(List<JTypeVar> tvars) {
assert this.typeParameters == null : "Type params were already parsed for " + this;
@ -105,6 +109,7 @@ abstract class GenericSigBase<T extends JTypeParameterOwnerSymbol & AsmStub> {
private static final String OBJECT_BOUND = ":" + OBJECT_SIG;
private final @Nullable String signature;
private final int typeParameterCount;
private @Nullable JClassType superType;
private List<JClassType> superItfs;
@ -116,8 +121,9 @@ abstract class GenericSigBase<T extends JTypeParameterOwnerSymbol & AsmStub> {
@Nullable String signature, // null if doesn't use generics in header
@Nullable String superInternalName, // null if this is the Object class
String[] interfaces) {
super(ctx);
super(ctx, "LazyClassSignature:" + ctx.getInternalName() + "[" + signature + "]");
this.signature = signature;
this.typeParameterCount = GenericTypeParameterCounter.determineTypeParameterCount(this.signature);
this.rawItfs = CollectionUtil.map(interfaces, ctx.getResolver()::resolveFromInternalNameCannotFail);
this.rawSuper = ctx.getResolver().resolveFromInternalNameCannotFail(superInternalName);
@ -157,8 +163,9 @@ abstract class GenericSigBase<T extends JTypeParameterOwnerSymbol & AsmStub> {
}
@Override
protected boolean isGeneric() {
return signature != null && TypeParamsParser.hasTypeParams(signature);
protected int getTypeParameterCount() {
// note: no ensureParsed() needed, the type parameters are counted eagerly
return typeParameterCount;
}
@Override
@ -206,6 +213,7 @@ abstract class GenericSigBase<T extends JTypeParameterOwnerSymbol & AsmStub> {
static class LazyMethodType extends GenericSigBase<ExecutableStub> implements TypeAnnotationReceiver {
private final @NonNull String signature;
private final int typeParameterCount;
private @Nullable TypeAnnotationSet receiverAnnotations;
private List<JTypeMirror> parameterTypes;
@ -233,8 +241,9 @@ abstract class GenericSigBase<T extends JTypeParameterOwnerSymbol & AsmStub> {
@Nullable String genericSig,
@Nullable String[] exceptions,
boolean skipFirstParam) {
super(ctx);
super(ctx, "LazyMethodType:" + (genericSig != null ? genericSig : descriptor));
this.signature = genericSig != null ? genericSig : descriptor;
this.typeParameterCount = GenericTypeParameterCounter.determineTypeParameterCount(genericSig);
// generic signatures already omit the synthetic param
this.skipFirstParam = skipFirstParam && genericSig == null;
this.rawExceptions = exceptions;
@ -288,8 +297,9 @@ abstract class GenericSigBase<T extends JTypeParameterOwnerSymbol & AsmStub> {
@Override
protected boolean isGeneric() {
return TypeParamsParser.hasTypeParams(signature);
protected int getTypeParameterCount() {
// note: no ensureParsed() needed, the type parameters are counted eagerly
return typeParameterCount;
}
void setParameterTypes(List<JTypeMirror> params) {

View File

@ -0,0 +1,36 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.java.symbols.internal.asm;
import org.objectweb.asm.signature.SignatureReader;
import org.objectweb.asm.signature.SignatureVisitor;
class GenericTypeParameterCounter extends SignatureVisitor {
private int count;
GenericTypeParameterCounter() {
super(AsmSymbolResolver.ASM_API_V);
}
@Override
public void visitFormalTypeParameter(String name) {
count++;
}
public int getCount() {
return count;
}
static int determineTypeParameterCount(String signature) {
if (signature == null) {
return 0;
}
SignatureReader signatureReader = new SignatureReader(signature);
GenericTypeParameterCounter counter = new GenericTypeParameterCounter();
signatureReader.accept(counter);
return counter.getCount();
}
}

View File

@ -35,7 +35,7 @@ class ModuleStub implements JModuleSymbol, AsmStub, AnnotationOwner {
this.resolver = resolver;
this.moduleName = moduleName;
this.parseLock = new ParseLock() {
this.parseLock = new ParseLock("ModuleStub:" + moduleName) {
@Override
protected boolean doParse() throws IOException {
try (InputStream instream = loader.getInputStream()) {

View File

@ -17,15 +17,30 @@ abstract class ParseLock {
private static final Logger LOG = LoggerFactory.getLogger(ParseLock.class);
private volatile ParseStatus status = ParseStatus.NOT_PARSED;
private final String name;
protected ParseLock(String name) {
this.name = name;
}
public void ensureParsed() {
getFinalStatus();
}
private void logParseLockTrace(String prefix) {
if (LOG.isTraceEnabled()) {
LOG.trace("{} {}: {}", Thread.currentThread().getName(), String.format("%-15s", prefix), this);
}
}
private ParseStatus getFinalStatus() {
ParseStatus status = this.status;
if (!status.isFinished) {
logParseLockTrace("waiting on");
synchronized (this) {
logParseLockTrace("locked");
status = this.status;
if (status == ParseStatus.NOT_PARSED) {
this.status = ParseStatus.BEING_PARSED;
@ -54,6 +69,7 @@ abstract class ParseLock {
throw new IllegalStateException("Thread is reentering the parse lock");
}
}
logParseLockTrace("released");
}
return status;
}
@ -85,7 +101,7 @@ abstract class ParseLock {
@Override
public String toString() {
return "ParseLock{status=" + status + '}';
return "ParseLock{name=" + name + ",status=" + status + '}';
}
private enum ParseStatus {

View File

@ -717,6 +717,12 @@ public final class TypeOps {
// no unchecked warning.
return allArgsAreUnboundedWildcards(sargs) ? Convertibility.UNCHECKED_NO_WARNING
: Convertibility.UNCHECKED_WARNING;
} else if (sargs.isEmpty()) {
// C<T1...TN> <: |C|
// JLS 4.10.2
// unchecked conversion converts a raw type to a generic type
// subtyping converts a generic type to its raw type
return Convertibility.SUBTYPING;
}
if (targs.size() != sargs.size()) {

View File

@ -335,6 +335,13 @@ final class ExprCheckHelper {
checker.checkExprConstraint(infCtx, capture(r2), r);
}
completeMethodRefInference(mref, nonWildcard, fun, exactMethod, true);
} else if (TypeOps.isUnresolved(mref.getTypeToSearch())) {
// Then this is neither an exact nor inexact method ref,
// we just don't know what it is.
// The return values of the mref are assimilated to an (*unknown*) type.
checker.checkExprConstraint(infCtx, ts.UNKNOWN, fun.getReturnType());
completeMethodRefInference(mref, nonWildcard, fun, ts.UNRESOLVED_METHOD, false);
} else {
// Otherwise, the method reference is inexact, and:
@ -552,8 +559,15 @@ final class ExprCheckHelper {
// finally, add bounds
if (result != ts.NO_TYPE) {
Set<InferenceVar> inputIvars = infCtx.freeVarsIn(groundFun.getFormalParameters());
// The free vars of the return type depend on the free vars of the parameters.
// This explicit dependency is there to prevent solving the variables in the
// return type before solving those of the parameters. That is because the variables
// mentioned in the return type may be further constrained by adding the return constraints
// below (in the listener), which is only triggered when the input ivars have been instantiated.
infCtx.addInstantiationDependencies(infCtx.freeVarsIn(groundFun.getReturnType()), inputIvars);
infCtx.addInstantiationListener(
infCtx.freeVarsIn(groundFun.getFormalParameters()),
inputIvars,
solvedCtx -> {
if (mayMutateExpr()) {
lambda.setInferredType(solvedCtx.ground(groundTargetType));
@ -562,8 +576,15 @@ final class ExprCheckHelper {
lambda.updateTypingContext(solvedGroundFun);
}
JTypeMirror groundResult = solvedCtx.ground(result);
// We need to build another checker that uses the solved context.
// This is because the free vars may have been adopted by a parent
// context, so the solvedCtx may be that parent context. The checks
// must use that context so that constraints and listeners are added
// to the parent context, since that one is responsible for solving
// the variables.
ExprCheckHelper newChecker = new ExprCheckHelper(solvedCtx, phase, this.checker, site, infer);
for (ExprMirror expr : lambda.getResultExpressions()) {
if (!isCompatible(groundResult, expr)) {
if (!newChecker.isCompatible(groundResult, expr)) {
return;
}
}

View File

@ -227,7 +227,6 @@ final class ExprOps {
}
} else {
JClassType enclosing = mref.getEnclosingType();
accessible = mref.getTypeToSearch()
.streamMethods(TypeOps.accessibleMethodFilter(mref.getMethodName(), enclosing.getSymbol()))
.collect(OverloadSet.collectMostSpecific(enclosing));

View File

@ -36,7 +36,6 @@ import net.sourceforge.pmd.lang.java.types.internal.infer.ExprMirror.CtorInvocat
import net.sourceforge.pmd.lang.java.types.internal.infer.ExprMirror.FunctionalExprMirror;
import net.sourceforge.pmd.lang.java.types.internal.infer.ExprMirror.InvocationMirror;
import net.sourceforge.pmd.lang.java.types.internal.infer.ExprMirror.InvocationMirror.MethodCtDecl;
import net.sourceforge.pmd.lang.java.types.internal.infer.ExprMirror.LambdaExprMirror;
import net.sourceforge.pmd.lang.java.types.internal.infer.ExprMirror.MethodRefMirror;
import net.sourceforge.pmd.lang.java.types.internal.infer.ExprMirror.PolyExprMirror;
import net.sourceforge.pmd.lang.java.types.internal.infer.InferenceVar.BoundKind;
@ -145,13 +144,19 @@ public final class Infer {
LOG.logResolutionFail(rfe.getFailure());
// here we set expected if not null, the lambda will have the target type
expr.setInferredType(expected == null ? ts.UNKNOWN : expected);
expr.setFunctionalMethod(ts.UNRESOLVED_METHOD);
if (expr instanceof MethodRefMirror) {
MethodRefMirror mref = (MethodRefMirror) expr;
mref.setFunctionalMethod(ts.UNRESOLVED_METHOD);
if (!TypeOps.isUnresolved(mref.getTypeToSearch())) {
JMethodSig exactMethod = ExprOps.getExactMethod(mref);
if (exactMethod != null) {
// as a fallback, if the method reference is exact,
// we populate the compile time decl anyway.
mref.setCompileTimeDecl(exactMethod);
return;
}
}
mref.setCompileTimeDecl(ts.UNRESOLVED_METHOD);
} else {
LambdaExprMirror lambda = (LambdaExprMirror) expr;
lambda.setFunctionalMethod(ts.UNRESOLVED_METHOD);
}
}
}
@ -602,17 +607,20 @@ public final class Infer {
// see: https://docs.oracle.com/javase/specs/jls/se9/html/jls-18.html#jls-18.5.1
// as per https://docs.oracle.com/javase/specs/jls/se9/html/jls-18.html#jls-18.5.2
// we only test it can reduce, we don't commit inferred types at this stage
InferenceContext ctxCopy = infCtx.copy();
LOG.applicabilityTest(ctxCopy, m);
ctxCopy.solve(/*onlyBoundedVars:*/isPreJava8());
InferenceContext ctxCopy = infCtx.shallowCopy();
LOG.applicabilityTest(ctxCopy);
try {
ctxCopy.solve(/*onlyBoundedVars:*/isPreJava8());
} finally {
LOG.finishApplicabilityTest();
}
// if unchecked conversion was needed, update the site for invocation pass
if (ctxCopy.needsUncheckedConversion()) {
site.setNeedsUncheckedConversion();
}
// don't commit any types
return m;
return infCtx.mapToIVars(m);
}
} finally {
// Note that even if solve succeeded, listeners checking deferred

View File

@ -13,11 +13,13 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Supplier;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
@ -38,6 +40,7 @@ import net.sourceforge.pmd.lang.java.types.internal.infer.IncorporationAction.Pr
import net.sourceforge.pmd.lang.java.types.internal.infer.IncorporationAction.SubstituteInst;
import net.sourceforge.pmd.lang.java.types.internal.infer.InferenceVar.BoundKind;
import net.sourceforge.pmd.lang.java.types.internal.infer.VarWalkStrategy.GraphWalk;
import net.sourceforge.pmd.util.CollectionUtil;
/**
* Context of a type inference process. This object maintains a set of
@ -51,6 +54,13 @@ final class InferenceContext {
private static int ctxId = 0;
private final Map<InstantiationListener, Set<InferenceVar>> instantiationListeners = new HashMap<>();
// explicit dependencies between variables for graph building
private final Map<InferenceVar, Set<InferenceVar>> instantiationConstraints = new HashMap<>();
// This flag is set to true when the explicit dependencies are changed,
// or when this context adopted new ivars. This means we should interrupt
// resolution and recompute the dependency graph between ivars, because
// the new variables may have dependencies on existing variables, and vice versa.
private boolean graphWasChanged = false;
private final Set<InferenceVar> freeVars = new LinkedHashSet<>();
private final Set<InferenceVar> inferenceVars = new LinkedHashSet<>();
@ -127,18 +137,19 @@ final class InferenceContext {
}
}
public InferenceContext copy() {
/**
* Performs a shallow copy of this context, which would allow solving
* the variables without executing listeners. Instantiation listeners
* are not copied, and parent contexts are not copied.
*/
public InferenceContext shallowCopy() {
final InferenceContext copy = new InferenceContext(ts, supertypeCheckCache, Collections.emptyList(), logger);
copy.freeVars.addAll(this.freeVars);
copy.inferenceVars.addAll(this.inferenceVars);
copy.incorporationActions.addAll(this.incorporationActions);
copy.instantiationConstraints.putAll(this.instantiationConstraints);
copy.mapping = mapping; // mapping is immutable, so we can share it safely
// recursively copy parents
if (this.parent != null) {
copy.parent = this.parent.copy();
}
return copy;
}
@ -310,10 +321,20 @@ final class InferenceContext {
* Copy variable in this inference context to the given context
*/
void duplicateInto(final InferenceContext that) {
boolean changedGraph = !that.freeVars.containsAll(this.freeVars)
|| !this.instantiationConstraints.isEmpty();
that.graphWasChanged |= changedGraph;
that.inferenceVars.addAll(this.inferenceVars);
that.freeVars.addAll(this.freeVars);
that.incorporationActions.addAll(this.incorporationActions);
that.instantiationListeners.putAll(this.instantiationListeners);
CollectionUtil.mergeMaps(
that.instantiationConstraints,
this.instantiationConstraints,
(set1, set2) -> {
set1.addAll(set2);
return set1;
});
this.parent = that;
@ -324,6 +345,30 @@ final class InferenceContext {
}
// The `from` ivars depend on the `dependencies` ivars for resolution.
void addInstantiationDependencies(Set<? extends InferenceVar> from, Set<? extends InferenceVar> dependencies) {
if (from.isEmpty()) {
return;
}
Set<InferenceVar> outputVars = new HashSet<>(dependencies);
outputVars.removeAll(from);
if (outputVars.isEmpty()) {
return;
}
for (InferenceVar inputVar : from) {
logger.ivarDependencyRegistered(this, inputVar, outputVars);
instantiationConstraints.merge(inputVar, outputVars, (o1, o2) -> {
o2 = new LinkedHashSet<>(o2);
o2.addAll(o1);
return o2;
});
}
}
Map<InferenceVar, Set<InferenceVar>> getInstantiationDependencies() {
return instantiationConstraints;
}
void addInstantiationListener(Set<? extends JTypeMirror> relevantTypes, InstantiationListener listener) {
Set<InferenceVar> free = freeVarsIn(relevantTypes);
if (free.isEmpty()) {
@ -448,7 +493,7 @@ final class InferenceContext {
}
boolean solve(boolean onlyBoundedVars) {
return solve(new GraphWalk(this, onlyBoundedVars));
return solve(() -> new GraphWalk(this, onlyBoundedVars));
}
/**
@ -459,7 +504,28 @@ final class InferenceContext {
solve(new GraphWalk(var));
}
private boolean solve(Supplier<VarWalkStrategy> newWalker) {
VarWalkStrategy strategy = newWalker.get();
while (strategy != null) {
if (solve(strategy)) {
break;
}
strategy = newWalker.get();
}
return freeVars.isEmpty();
}
/**
* This returns true if solving the VarWalkStrategy succeeded entirely.
* Resolution can be interrupted early to account for new ivars and dependencies,
* which may change the graph dependencies. In this case this method returns
* false, we recompute the graph with the new ivars and dependencies, and
* we try again to make progress.
*/
private boolean solve(VarWalkStrategy walker) {
graphWasChanged = false;
incorporate();
while (walker.hasNext()) {
@ -470,6 +536,12 @@ final class InferenceContext {
//repeat until all variables are solved
outer:
while (!intersect(freeVars, varsToSolve).isEmpty() && progress) {
if (graphWasChanged) {
graphWasChanged = false;
logger.contextDependenciesChanged(this);
return false;
}
progress = false;
for (List<ReductionStep> wave : ReductionStep.WAVES) {
if (solveBatchProgressed(varsToSolve, wave)) {
@ -481,7 +553,7 @@ final class InferenceContext {
}
}
}
return freeVars.isEmpty();
return true;
}
/**

View File

@ -12,6 +12,7 @@ import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -21,6 +22,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import net.sourceforge.pmd.lang.java.ast.JavaNode;
import net.sourceforge.pmd.lang.java.internal.JavaLanguageProperties;
import net.sourceforge.pmd.lang.java.symbols.JTypeDeclSymbol;
import net.sourceforge.pmd.lang.java.types.JMethodSig;
import net.sourceforge.pmd.lang.java.types.JTypeMirror;
@ -33,6 +35,12 @@ import net.sourceforge.pmd.util.StringUtil;
/**
* A strategy to log the execution traces of {@link Infer}.
* The default does nothing, so the logger calls can be optimized out
* at runtime, while not having to check that logging is enabled at the
* call sites.
*
* <p>To enable logging for the CLI, use the language property ({@link JavaLanguageProperties})
* {@code xTypeInferenceLogging}. From tests, see {@code JavaParsingHelper#logTypeInferenceVerbose()}.
*/
@SuppressWarnings("PMD.UncommentedEmptyMethodBody")
public interface TypeInferenceLogger {
@ -61,7 +69,9 @@ public interface TypeInferenceLogger {
default void ctxInitialization(InferenceContext ctx, JMethodSig sig) { }
default void applicabilityTest(InferenceContext ctx, JMethodSig sig) { }
default void applicabilityTest(InferenceContext ctx) { }
default void finishApplicabilityTest() { }
default void startArgsChecks() { }
@ -81,6 +91,8 @@ public interface TypeInferenceLogger {
default void propagateAndAbort(InferenceContext context, InferenceContext parent) { }
default void contextDependenciesChanged(InferenceContext ctx) { }
// ivar events
@ -90,6 +102,8 @@ public interface TypeInferenceLogger {
default void ivarInstantiated(InferenceContext ctx, InferenceVar var, JTypeMirror inst) { }
default void ivarDependencyRegistered(InferenceContext ctx, InferenceVar var, Set<InferenceVar> deps) { }
/**
* Log that the instantiation of the method type m for the given
@ -136,9 +150,11 @@ public interface TypeInferenceLogger {
protected final PrintStream out;
protected static final int LEVEL_INCREMENT = 4;
private int level;
private String indent;
/**
* Four spaces.
*/
protected static final String BASE_INDENT = " ";
protected static final String ANSI_RESET = "\u001B[0m";
protected static final String ANSI_BLUE = "\u001B[34m";
@ -177,16 +193,24 @@ public interface TypeInferenceLogger {
public SimpleLogger(PrintStream out) {
this.out = out;
updateLevel(0);
this.indent = "";
}
protected int getLevel() {
return level;
protected void addIndentSegment(String segment) {
indent += segment;
}
protected void updateLevel(int increment) {
level += increment;
indent = StringUtils.repeat(' ', level);
protected void removeIndentSegment(String segment) {
assert indent.endsWith(segment) : "mismatched end section!";
indent = StringUtils.removeEnd(indent, segment);
}
protected void setIndent(String indent) {
this.indent = indent;
}
protected String getIndent() {
return indent;
}
protected void println(String str) {
@ -196,13 +220,13 @@ public interface TypeInferenceLogger {
protected void endSection(String footer) {
updateLevel(-LEVEL_INCREMENT);
removeIndentSegment(BASE_INDENT);
println(footer);
}
protected void startSection(String header) {
println(header);
updateLevel(+LEVEL_INCREMENT);
addIndentSegment(BASE_INDENT);
}
@Override
@ -335,7 +359,7 @@ public interface TypeInferenceLogger {
class VerboseLogger extends SimpleLogger {
private final Deque<Integer> marks = new ArrayDeque<>();
private final Deque<String> marks = new ArrayDeque<>();
public VerboseLogger(PrintStream out) {
super(out);
@ -343,16 +367,16 @@ public interface TypeInferenceLogger {
}
void mark() {
marks.push(getLevel());
marks.push(getIndent());
}
void rollback(String lastWords) {
int pop = marks.pop();
updateLevel(pop - getLevel()); // back to normal
final String savedIndent = marks.pop();
setIndent(savedIndent); // back to normal
if (!lastWords.isEmpty()) {
updateLevel(+LEVEL_INCREMENT);
addIndentSegment(BASE_INDENT);
println(lastWords);
updateLevel(-LEVEL_INCREMENT);
setIndent(savedIndent);
}
}
@ -369,8 +393,14 @@ public interface TypeInferenceLogger {
}
@Override
public void applicabilityTest(InferenceContext ctx, JMethodSig sig) {
println(String.format("Applicability testing with Context %-11d%s", ctx.getId(), ppHighlight(ctx.mapToIVars(sig))));
public void applicabilityTest(InferenceContext ctx) {
println(String.format("Solving with context %d for applicability testing", ctx.getId()));
addIndentSegment("| ");
}
@Override
public void finishApplicabilityTest() {
removeIndentSegment("| ");
}
@Override
@ -404,7 +434,7 @@ public interface TypeInferenceLogger {
@Override
public void startArg(int i, ExprMirror expr, JTypeMirror formalType) {
startSection("Checking arg " + i + " against " + formalType);
startSection("Checking arg " + i + " against " + colorIvars(formalType));
printExpr(expr);
}
@ -452,6 +482,16 @@ public interface TypeInferenceLogger {
println(addCtxInfo(ctx, "Ivar instantiated") + color(var + " := ", ANSI_BLUE) + colorIvars(inst));
}
@Override
public void ivarDependencyRegistered(InferenceContext ctx, InferenceVar var, Set<InferenceVar> deps) {
println(addCtxInfo(ctx, "Ivar dependency registered: ") + color(var + " -> ", ANSI_BLUE) + colorIvars(deps));
}
@Override
public void contextDependenciesChanged(InferenceContext ctx) {
println("Recomputing dependency graph (ctx " + ctx.getId() + ")");
}
private @NonNull String addCtxInfo(InferenceContext ctx, String event) {
return String.format("%-20s(ctx %d): ", event, ctx.getId());
}

View File

@ -90,6 +90,14 @@ interface VarWalkStrategy extends Iterator<Set<InferenceVar>> {
}
}
ctx.getInstantiationDependencies().forEach((ivar, deps) -> {
Vertex<InferenceVar> vertex = graph.addLeaf(ivar);
for (InferenceVar dep : deps) {
Vertex<InferenceVar> target = graph.addLeaf(dep);
graph.addEdge(vertex, target);
}
});
// Here, "α depends on β" is modelled by an edge α -> β
// Merge strongly connected components into a "super node".

View File

@ -629,7 +629,7 @@ Note: This rule was named TooFewBranchesForASwitchStatement before PMD 7.7.0.
<value>
<![CDATA[
//(SwitchStatement | SwitchExpression)
[ (count(*) - 1) < $minimumNumberCaseForASwitch ]
[ (count(*/SwitchLabel/*) + count(*/SwitchLabel[@Default = true()])) < $minimumNumberCaseForASwitch ]
(: only consider if no pattern matching is used :)
[*/SwitchLabel[@PatternLabel = false()]]
]]>

View File

@ -0,0 +1,157 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.java.symbols;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import net.sourceforge.pmd.lang.java.JavaParsingHelper;
import net.sourceforge.pmd.lang.java.ast.ASTClassType;
import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
/**
* Tests to help analyze [java] Deadlock when executing PMD in multiple threads #5293.
*
* @see <a href="https://github.com/pmd/pmd/issues/5293">[java] Deadlock when executing PMD in multiple threads #5293</a>
*/
class DeadlockTest {
abstract static class Outer<T> implements GenericInterface<Outer<T>, GenericClass<T>> {
// must be a nested class, that is reusing the type param T of the outer class
abstract static class Inner<T> {
Inner(Outer<T> grid) { }
}
}
static class GenericBaseClass<T> { }
interface GenericInterface<T, S> { }
abstract static class GenericClass<T> extends GenericBaseClass<Outer.Inner<T>> { }
@Test
@Timeout(2)
void parseWithoutDeadlock() throws InterruptedException {
/*
Deadlock:
t1 -> locks parse for Outer.Inner and waits for parse lock for Outer
t1 waiting on : ParseLock{name=LazyClassSignature:net/sourceforge/pmd/lang/java/symbols/DeadlockTest$Outer$Inner[<T:Ljava/lang/Object;>Ljava/lang/Object;],status=NOT_PARSED}
t1 locked : ParseLock{name=LazyClassSignature:net/sourceforge/pmd/lang/java/symbols/DeadlockTest$Outer$Inner[<T:Ljava/lang/Object;>Ljava/lang/Object;],status=NOT_PARSED}
t1 waiting on : ParseLock{name=LazyClassSignature:net/sourceforge/pmd/lang/java/symbols/DeadlockTest$Outer[<T:Ljava/lang/Object;>Ljava/lang/Object;Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$GenericInterface<Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$Outer<TT;>;Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$GenericClass<TT;>;>;],status=BEING_PARSED}
t2 -> locks parse for Outer, locks parse for GenericInterface and then waits for parse lock for Outer.Inner
t2 waiting on : ParseLock{name=LazyClassSignature:net/sourceforge/pmd/lang/java/symbols/DeadlockTest$Outer[<T:Ljava/lang/Object;>Ljava/lang/Object;Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$GenericInterface<Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$Outer<TT;>;Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$GenericClass<TT;>;>;],status=NOT_PARSED}
t2 locked : ParseLock{name=LazyClassSignature:net/sourceforge/pmd/lang/java/symbols/DeadlockTest$Outer[<T:Ljava/lang/Object;>Ljava/lang/Object;Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$GenericInterface<Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$Outer<TT;>;Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$GenericClass<TT;>;>;],status=NOT_PARSED}
t2 waiting on : ParseLock{name=LazyClassSignature:net/sourceforge/pmd/lang/java/symbols/DeadlockTest[null],status=NOT_PARSED}
t2 locked : ParseLock{name=LazyClassSignature:net/sourceforge/pmd/lang/java/symbols/DeadlockTest[null],status=NOT_PARSED}
t2 released : ParseLock{name=LazyClassSignature:net/sourceforge/pmd/lang/java/symbols/DeadlockTest[null],status=FULL}
t2 waiting on : ParseLock{name=LazyClassSignature:net/sourceforge/pmd/lang/java/symbols/DeadlockTest$Outer[<T:Ljava/lang/Object;>Ljava/lang/Object;Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$GenericInterface<Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$Outer<TT;>;Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$GenericClass<TT;>;>;],status=BEING_PARSED}
t2 locked : ParseLock{name=LazyClassSignature:net/sourceforge/pmd/lang/java/symbols/DeadlockTest$Outer[<T:Ljava/lang/Object;>Ljava/lang/Object;Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$GenericInterface<Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$Outer<TT;>;Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$GenericClass<TT;>;>;],status=BEING_PARSED}
t2 released : ParseLock{name=LazyClassSignature:net/sourceforge/pmd/lang/java/symbols/DeadlockTest$Outer[<T:Ljava/lang/Object;>Ljava/lang/Object;Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$GenericInterface<Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$Outer<TT;>;Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$GenericClass<TT;>;>;],status=BEING_PARSED}
t2 waiting on : ParseLock{name=LazyClassSignature:net/sourceforge/pmd/lang/java/symbols/DeadlockTest$GenericClass[<T:Ljava/lang/Object;>Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$GenericBaseClass<Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$Outer$Inner<TT;>;>;],status=NOT_PARSED}
t2 locked : ParseLock{name=LazyClassSignature:net/sourceforge/pmd/lang/java/symbols/DeadlockTest$GenericClass[<T:Ljava/lang/Object;>Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$GenericBaseClass<Lnet/sourceforge/pmd/lang/java/symbols/DeadlockTest$Outer$Inner<TT;>;>;],status=NOT_PARSED}
t2 waiting on : ParseLock{name=LazyClassSignature:net/sourceforge/pmd/lang/java/symbols/DeadlockTest$Outer$Inner[<T:Ljava/lang/Object;>Ljava/lang/Object;],status=NOT_PARSED}
In order to reproduce the deadlock reliably, add the following piece into ParseLock, just at the beginning
of the synchronized block (line 42):
// t1 needs to wait after having the lock, so that t2 can go on and wait on the same lock
if (Thread.currentThread().getName().equals("t1") && this.toString().contains("LazyClassSignature:net/sourceforge/pmd/lang/java/symbols/DeadlockTest$Outer$Inner[<T:L")) {
try {
Thread.sleep(500);
} catch (InterruptedException ignored) {
}
}
And then, introduce a bug again. One way to make the test fail is:
Comment out the method "public int getTypeParameterCount()", so that it is inherited again.
Add the following method:
@Override
public boolean isGeneric() {
parseLock.ensureParsed();
return signature.isGeneric();
}
*/
List<Throwable> exceptions = new ArrayList<>();
Thread.UncaughtExceptionHandler exceptionHandler = (t, e) -> {
exceptions.add(e);
e.printStackTrace();
};
Thread t1 = new Thread(() -> {
ASTCompilationUnit class1 = JavaParsingHelper.DEFAULT.parse(
"package net.sourceforge.pmd.lang.java.symbols;\n"
+ "import net.sourceforge.pmd.lang.java.symbols.DeadlockTest.Outer;\n"
+ " class Class1 {\n"
+ " public static <X> Outer.Inner<X> newInner(Outer<X> grid) {\n"
+ " return null;\n"
+ " }\n"
+ " }\n"
);
assertNotNull(class1);
// Outer.Inner<X> = return type of method "newInner"
List<ASTClassType> classTypes = class1.descendants(ASTClassType.class).toList();
ASTClassType outerInner = classTypes.get(0);
assertGenericClassType(outerInner, "Inner", "X", "T");
// Outer = qualifier of Outer.Inner<X>
ASTClassType outer = classTypes.get(1);
assertEquals("Outer", outer.getSimpleName());
assertNull(outer.getTypeArguments());
// Outer<X> = formal parameter type of method newInner
ASTClassType outerFormalParam = classTypes.get(3);
assertGenericClassType(outerFormalParam, "Outer", "X", "T");
}, "t1");
t1.setUncaughtExceptionHandler(exceptionHandler);
Thread t2 = new Thread(() -> {
ASTCompilationUnit class2 = JavaParsingHelper.DEFAULT.parse(
"package net.sourceforge.pmd.lang.java.symbols;\n"
+ "import net.sourceforge.pmd.lang.java.symbols.DeadlockTest.Outer;\n"
+ " class Class2<M> {\n"
+ " protected Outer<M> theOuter;\n"
+ " }\n"
);
assertNotNull(class2);
// Outer<M> = type of field "theOuter"
ASTClassType firstClassType = class2.descendants(ASTClassType.class).first();
assertNotNull(firstClassType);
assertGenericClassType(firstClassType, "Outer", "M", "T");
}, "t2");
t2.setUncaughtExceptionHandler(exceptionHandler);
t1.start();
t2.start();
t1.join();
t2.join();
assertAll(exceptions.stream()
.map(e -> () -> {
throw e;
}));
}
private static void assertGenericClassType(ASTClassType classType, String simpleName, String actualTypeParamName, String originalTypeParamName) {
assertEquals(simpleName, classType.getSimpleName());
assertEquals(1, classType.getTypeArguments().size());
assertEquals(actualTypeParamName, ((ASTClassType) classType.getTypeArguments().get(0)).getSimpleName());
JTypeParameterOwnerSymbol symbol = (JTypeParameterOwnerSymbol) classType.getTypeMirror().getSymbol();
assertEquals(1, symbol.getTypeParameterCount());
assertEquals(originalTypeParamName, symbol.getTypeParameters().get(0).getName());
}
}

Some files were not shown because too many files have changed in this diff Show More