Merge branch 'main' into issue-5287
This commit is contained in:
10
Gemfile.lock
10
Gemfile.lock
@ -45,7 +45,7 @@ GEM
|
||||
git (1.19.1)
|
||||
addressable (~> 2.8)
|
||||
rchardet (~> 1.8)
|
||||
json (2.7.2)
|
||||
json (2.7.5)
|
||||
kramdown (2.4.0)
|
||||
rexml
|
||||
kramdown-parser-gfm (1.1.0)
|
||||
@ -74,10 +74,10 @@ GEM
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rchardet (1.8.0)
|
||||
rexml (3.3.8)
|
||||
rexml (3.3.9)
|
||||
rouge (4.4.0)
|
||||
rufus-scheduler (3.9.1)
|
||||
fugit (~> 1.1, >= 1.1.6)
|
||||
rufus-scheduler (3.9.2)
|
||||
fugit (~> 1.1, >= 1.11.1)
|
||||
safe_yaml (1.0.5)
|
||||
sawyer (0.9.2)
|
||||
addressable (>= 2.3.5)
|
||||
@ -102,4 +102,4 @@ DEPENDENCIES
|
||||
safe_yaml
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.3
|
||||
2.5.22
|
||||
|
@ -1,8 +1,9 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
activesupport (7.2.1)
|
||||
activesupport (7.2.2)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
@ -15,6 +16,7 @@ GEM
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
base64 (0.2.0)
|
||||
benchmark (0.3.0)
|
||||
bigdecimal (3.1.8)
|
||||
coffee-script (2.4.1)
|
||||
coffee-script-source
|
||||
@ -34,9 +36,10 @@ GEM
|
||||
ethon (0.16.0)
|
||||
ffi (>= 1.15.0)
|
||||
eventmachine (1.2.7)
|
||||
execjs (2.9.1)
|
||||
faraday (2.11.0)
|
||||
execjs (2.10.0)
|
||||
faraday (2.12.0)
|
||||
faraday-net_http (>= 2.0, < 3.4)
|
||||
json
|
||||
logger
|
||||
faraday-net_http (3.3.0)
|
||||
net-http
|
||||
@ -99,7 +102,7 @@ GEM
|
||||
activesupport (>= 2)
|
||||
nokogiri (>= 1.4)
|
||||
http_parser.rb (0.8.0)
|
||||
i18n (1.14.5)
|
||||
i18n (1.14.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jekyll (3.10.0)
|
||||
addressable (~> 2.4)
|
||||
@ -211,6 +214,7 @@ GEM
|
||||
gemoji (>= 3, < 5)
|
||||
html-pipeline (~> 2.2)
|
||||
jekyll (>= 3.0, < 5.0)
|
||||
json (2.7.5)
|
||||
kramdown (2.4.0)
|
||||
rexml
|
||||
kramdown-parser-gfm (1.1.0)
|
||||
@ -219,7 +223,7 @@ GEM
|
||||
listen (3.9.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
logger (1.6.0)
|
||||
logger (1.6.1)
|
||||
mercenary (0.3.6)
|
||||
minima (2.5.1)
|
||||
jekyll (>= 3.5, < 5.0)
|
||||
@ -240,8 +244,7 @@ GEM
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.11.1)
|
||||
ffi (~> 1.0)
|
||||
rexml (3.3.6)
|
||||
strscan
|
||||
rexml (3.3.9)
|
||||
rouge (3.30.0)
|
||||
rubyzip (2.3.2)
|
||||
safe_yaml (1.0.5)
|
||||
@ -255,7 +258,6 @@ GEM
|
||||
faraday (>= 0.17.3, < 3)
|
||||
securerandom (0.3.1)
|
||||
simpleidn (0.2.3)
|
||||
strscan (3.1.0)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
typhoeus (1.4.1)
|
||||
@ -264,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
|
||||
@ -276,4 +278,4 @@ DEPENDENCIES
|
||||
webrick
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.3
|
||||
2.5.22
|
||||
|
@ -15,11 +15,20 @@ This is a {{ site.pmd.release_type }} release.
|
||||
### 🚀 New and noteworthy
|
||||
|
||||
### 🐛 Fixed Issues
|
||||
* ant
|
||||
* [#1860](https://github.com/pmd/pmd/issues/1860): \[ant] Reflective access warnings on java > 9 and java < 17
|
||||
* java
|
||||
* [#5293](https://github.com/pmd/pmd/issues/5293): \[java] Deadlock when executing PMD in multiple threads
|
||||
* java-performance
|
||||
* [#5287](https://github.com/pmd/pmd/issues/5287): \[java] TooFewBranchesForSwitch false-positive with switch using list of case constants
|
||||
|
||||
### 🚨 API Changes
|
||||
|
||||
#### Deprecations
|
||||
* pmd-xml
|
||||
* {%jdoc xml::lang.xml.antlr4.XMLLexer %} is deprecated for removal. Use {%jdoc !!xml::lang.xml.ast.XMLLexer %}
|
||||
instead (note different package `ast` instead of `antlr4`).
|
||||
|
||||
### ✨ External Contributions
|
||||
|
||||
{% endtocmaker %}
|
||||
|
@ -11,6 +11,7 @@ import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.Charset;
|
||||
@ -21,8 +22,6 @@ import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
import org.apache.commons.lang3.reflect.MethodUtils;
|
||||
import org.apache.tools.ant.BuildException;
|
||||
import org.apache.tools.ant.Project;
|
||||
import org.apache.tools.ant.types.Parameter;
|
||||
@ -200,10 +199,12 @@ public class Formatter {
|
||||
if (console != null) {
|
||||
// Since Java 22, this returns a console even for redirected streams.
|
||||
// In that case, we need to check Console.isTerminal()
|
||||
// https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/io/Console.html#isTerminal()
|
||||
// See: JLine As The Default Console Provider (JDK-8308591)
|
||||
try {
|
||||
Boolean isTerminal = (Boolean) MethodUtils.invokeMethod(console, "isTerminal");
|
||||
if (!isTerminal) {
|
||||
Method method = Console.class.getMethod("isTerminal");
|
||||
Object isTerminal = method.invoke(console);
|
||||
if (isTerminal instanceof Boolean && !(Boolean) isTerminal) {
|
||||
// stop here, we don't have an interactive console.
|
||||
return null;
|
||||
}
|
||||
@ -211,39 +212,58 @@ public class Formatter {
|
||||
// fall-through - we use a Java Runtime < 22.
|
||||
}
|
||||
|
||||
// Maybe this is Java17+? Then there will be a public method charset()
|
||||
// https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/Console.html#charset()
|
||||
try {
|
||||
Object res = FieldUtils.readDeclaredField(console, "cs", true);
|
||||
if (res instanceof Charset) {
|
||||
return ((Charset) res).name();
|
||||
Method method = Console.class.getMethod("charset");
|
||||
Object charset = method.invoke(console);
|
||||
if (charset instanceof Charset) {
|
||||
return ((Charset) charset).name();
|
||||
}
|
||||
} catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException ignored) {
|
||||
// fall-through
|
||||
}
|
||||
|
||||
{
|
||||
// try to use the system property "sun.jnu.encoding", which is the platform encoding.
|
||||
// this property is not specified and might not always be available, but it is for
|
||||
// openjdk 11: https://github.com/openjdk/jdk11u/blob/cee8535a9d3de8558b4b5028d68e397e508bef71/src/java.base/share/native/libjava/System.c#L384
|
||||
// if it exists, we use it - this avoids illegal reflective access below.
|
||||
String jnuEncoding = System.getProperty("sun.jnu.encoding");
|
||||
if (jnuEncoding != null) {
|
||||
return jnuEncoding;
|
||||
}
|
||||
}
|
||||
|
||||
// the following parts are accessing private/protected fields via reflection
|
||||
// this should work with Java 8 and 11. With Java 11, you'll see warnings abouts
|
||||
// illegal reflective access, see #1860. However, the access still works.
|
||||
|
||||
// Fall-Back 1: private field "cs" in java.io.Console
|
||||
try {
|
||||
Field field = Console.class.getDeclaredField("cs");
|
||||
field.setAccessible(true);
|
||||
Object csField = field.get(console);
|
||||
if (csField instanceof Charset) {
|
||||
return ((Charset) csField).name();
|
||||
}
|
||||
} catch (IllegalArgumentException | ReflectiveOperationException ignored) {
|
||||
// fall-through
|
||||
}
|
||||
|
||||
// Maybe this is Java17+? Then there will be
|
||||
// https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/Console.html#charset()
|
||||
// instead of the field "cs".
|
||||
// Fall-Back 2: private native method "encoding()" in java.io.Console
|
||||
try {
|
||||
Method charsetMethod = Console.class.getDeclaredMethod("charset");
|
||||
Charset charset = (Charset) charsetMethod.invoke(console);
|
||||
return charset.name();
|
||||
} catch (IllegalArgumentException | ReflectiveOperationException ignored) {
|
||||
Method method = Console.class.getDeclaredMethod("encoding");
|
||||
method.setAccessible(true);
|
||||
Object encoding = method.invoke(console);
|
||||
if (encoding instanceof String) {
|
||||
return (String) encoding;
|
||||
}
|
||||
} catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException ignored) {
|
||||
// fall-through
|
||||
}
|
||||
return getNativeConsoleEncoding();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String getNativeConsoleEncoding() {
|
||||
try {
|
||||
Object res = MethodUtils.invokeStaticMethod(Console.class, "encoding");
|
||||
if (res instanceof String) {
|
||||
return (String) res;
|
||||
}
|
||||
} catch (IllegalArgumentException | ReflectiveOperationException ignored) {
|
||||
// fall-through
|
||||
}
|
||||
// we couldn't determine the correct platform console encoding
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,8 @@
|
||||
package net.sourceforge.pmd.dist;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.containsStringIgnoringCase;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@ -40,9 +42,9 @@ class AntIT extends AbstractBinaryDistributionTest {
|
||||
|
||||
ExecutionResult result = runAnt(antBasepath, pmdHome, antTestProjectFolder);
|
||||
result.assertExitCode(0)
|
||||
.assertStdOut(containsString("BUILD SUCCESSFUL"));
|
||||
// the no package rule
|
||||
result.assertExitCode(0)
|
||||
.assertStdOut(containsString("BUILD SUCCESSFUL"))
|
||||
.assertStdOut(not(containsStringIgnoringCase("Illegal reflective access"))) // #1860
|
||||
// the no package rule
|
||||
.assertStdOut(containsString("NoPackage"));
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
5
pmd-java/src/test/resources/simplelogger.properties
Normal file
5
pmd-java/src/test/resources/simplelogger.properties
Normal file
@ -0,0 +1,5 @@
|
||||
#
|
||||
# BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
#
|
||||
|
||||
#org.slf4j.simpleLogger.log.net.sourceforge.pmd.lang.java.symbols.internal.asm.ParseLock=trace
|
@ -45,6 +45,28 @@
|
||||
</parameter>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>antlr-cleanup</id>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<target>
|
||||
<ant antfile="${antlr4.ant.wrapper}" target="cpd-language">
|
||||
<property name="lang-name" value="XML" />
|
||||
<property name="lang-id" value="xml" />
|
||||
</ant>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencies>
|
||||
|
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.lang.xml.antlr4;
|
||||
|
||||
import org.antlr.v4.runtime.CharStream;
|
||||
|
||||
/**
|
||||
* Backwards compatible bridge. The XMLLexer was moved to align it with other PMD modules.
|
||||
* This class will be removed in PMD 8.0.0.
|
||||
* Use {@link net.sourceforge.pmd.lang.xml.ast.XMLLexer} directly instead.
|
||||
*
|
||||
* @deprecated since 7.8.0. Use {@link net.sourceforge.pmd.lang.xml.ast.XMLLexer} directly instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public class XMLLexer extends net.sourceforge.pmd.lang.xml.ast.XMLLexer {
|
||||
public XMLLexer(CharStream input) {
|
||||
super(input);
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ import org.antlr.v4.runtime.CharStream;
|
||||
import org.antlr.v4.runtime.Lexer;
|
||||
|
||||
import net.sourceforge.pmd.cpd.impl.AntlrCpdLexer;
|
||||
import net.sourceforge.pmd.lang.xml.antlr4.XMLLexer;
|
||||
import net.sourceforge.pmd.lang.xml.ast.XMLLexer;
|
||||
|
||||
/**
|
||||
* <p>Note: This class has been called XmlTokenizer in PMD 6</p>.
|
||||
|
12
pom.xml
12
pom.xml
@ -101,7 +101,7 @@
|
||||
<surefire.version>3.2.5</surefire.version>
|
||||
<checkstyle.version>10.18.1</checkstyle.version>
|
||||
<checkstyle.plugin.version>3.5.0</checkstyle.plugin.version>
|
||||
<pmd.plugin.version>3.24.0</pmd.plugin.version>
|
||||
<pmd.plugin.version>3.26.0</pmd.plugin.version>
|
||||
<ant.version>1.10.14</ant.version>
|
||||
<javadoc.plugin.version>3.6.3</javadoc.plugin.version>
|
||||
<antlr.version>4.9.3</antlr.version>
|
||||
@ -179,7 +179,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<version>3.7.1</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
@ -202,7 +202,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-clean-plugin</artifactId>
|
||||
<version>3.3.2</version>
|
||||
<version>3.4.0</version>
|
||||
</plugin>
|
||||
|
||||
<!-- adds kotlin source directories for checkstyle header checks -->
|
||||
@ -592,11 +592,11 @@
|
||||
<artifactId>pmd-build-tools-config</artifactId>
|
||||
<version>${pmd.build-tools.version}</version>
|
||||
</dependency>
|
||||
<!-- Allow to build PMD with Java 23 -->
|
||||
<!-- Allow to build PMD with Java 24 -->
|
||||
<dependency>
|
||||
<groupId>org.ow2.asm</groupId>
|
||||
<artifactId>asm</artifactId>
|
||||
<version>9.7</version>
|
||||
<version>9.7.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
@ -828,7 +828,7 @@
|
||||
<dependency>
|
||||
<groupId>org.ow2.asm</groupId>
|
||||
<artifactId>asm</artifactId>
|
||||
<version>9.7</version>
|
||||
<version>9.7.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.pcollections</groupId>
|
||||
|
Reference in New Issue
Block a user