Merge branch 'main' into pr-5303
This commit is contained in:
commit
85aeebbab0
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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 %}
|
||||
|
||||
|
@ -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<>();
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 -->
|
||||
|
@ -21,4 +21,13 @@ class HtmlCpdLexerTest extends CpdTextComparisonTest {
|
||||
doTest("SimpleHtmlFile");
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidHtml() {
|
||||
doTest("InvalidHtml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void metaTag() {
|
||||
doTest("MetaTag");
|
||||
}
|
||||
}
|
||||
|
7
pmd-html/src/test/resources/net/sourceforge/pmd/lang/html/cpd/testdata/InvalidHtml.html
vendored
Normal file
7
pmd-html/src/test/resources/net/sourceforge/pmd/lang/html/cpd/testdata/InvalidHtml.html
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<body>
|
||||
<!-- missing closing tag for div -->
|
||||
<div class='wrapper'>
|
||||
</body>
|
||||
</html>
|
22
pmd-html/src/test/resources/net/sourceforge/pmd/lang/html/cpd/testdata/InvalidHtml.txt
vendored
Normal file
22
pmd-html/src/test/resources/net/sourceforge/pmd/lang/html/cpd/testdata/InvalidHtml.txt
vendored
Normal 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
|
9
pmd-html/src/test/resources/net/sourceforge/pmd/lang/html/cpd/testdata/MetaTag.html
vendored
Normal file
9
pmd-html/src/test/resources/net/sourceforge/pmd/lang/html/cpd/testdata/MetaTag.html
vendored
Normal 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>
|
27
pmd-html/src/test/resources/net/sourceforge/pmd/lang/html/cpd/testdata/MetaTag.txt
vendored
Normal file
27
pmd-html/src/test/resources/net/sourceforge/pmd/lang/html/cpd/testdata/MetaTag.txt
vendored
Normal 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
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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()) {
|
||||
|
@ -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 {
|
||||
|
@ -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()) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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".
|
||||
|
@ -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()]]
|
||||
]]>
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user