Merge pull request #3270 from oowekyala:java-classpath-resilience

[java] Improve resilience to broken classpath #3270
This commit is contained in:
Andreas Dangel committed 2021-05-13 10:49:19 +02:00
commit 5b814aa4b1
21 files changed
+415 -102

No files matched your search

@@ -175,7 +175,7 @@ public final class JavaAstProcessor {
SemanticErrorReporter logger,
TypeInferenceLogger typeInfLogger) {
TypeSystem typeSystem = TYPE_SYSTEMS.computeIfAbsent(classLoader, TypeSystem::new);
TypeSystem typeSystem = TYPE_SYSTEMS.computeIfAbsent(classLoader, TypeSystem::usingClassLoaderClasspath);
return new JavaAstProcessor(
typeSystem,
typeSystem.bootstrapResolver(),
@@ -27,7 +27,7 @@ public class AsmSymbolResolver implements SymbolResolver {
static final int ASM_API_V = Opcodes.ASM9;
private final TypeSystem ts;
private final ClassLoader classLoader;
private final Classpath classLoader;
private final SignatureParser typeLoader;
private final ConcurrentHashMap<String, SoftClassReference> knownStubs = new ConcurrentHashMap<>();
@@ -38,7 +38,7 @@ public class AsmSymbolResolver implements SymbolResolver {
*/
private final SoftClassReference failed;
public AsmSymbolResolver(TypeSystem ts, ClassLoader classLoader) {
public AsmSymbolResolver(TypeSystem ts, Classpath classLoader) {
this.ts = ts;
this.classLoader = classLoader;
this.typeLoader = new SignatureParser(this);
@@ -104,7 +104,7 @@ public class AsmSymbolResolver implements SymbolResolver {
@Nullable
URL getUrlOfInternalName(String internalName) {
return classLoader.getResource(internalName + ".class");
return classLoader.findResource(internalName + ".class");
}
/*
@@ -0,0 +1,72 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.java.symbols.internal.asm;
import java.net.URL;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* Classpath abstraction. PMD's symbol resolver uses the classpath to
* find class files.
*/
@FunctionalInterface
public interface Classpath {
/**
* Returns a URL to load the given resource if it exists in this classpath.
* Otherwise returns null. This will typically be used to find Java class files.
* A typical input would be {@code java/lang/String.class}.
*
* @param resourcePath Resource path, as described in {@link ClassLoader#getResource(String)}
*
* @return A URL if the resource exists, otherwise null
*/
@Nullable URL findResource(String resourcePath);
// <editor-fold defaultstate="collapsed" desc="Transformation methods (defaults)">
/**
* Return a classpath that will ignore the given classpath entries,
* even if they are present in this classpath. Every call to {@link #findResource(String)}
* is otherwise delegated to this one.
*
* @param deletedEntries Set of resource paths to exclude
*/
default Classpath exclude(Set<String> deletedEntries) {
return resourcePath -> deletedEntries.contains(resourcePath) ? null : findResource(resourcePath);
}
default Classpath delegateTo(Classpath c) {
return path -> {
URL p = findResource(path);
if (p != null) {
return p;
}
return c.findResource(path);
};
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc="Creator methods">
/**
* Returns a classpath instance that uses {@link ClassLoader#getResource(String)}
* to find resources.
*/
static Classpath forClassLoader(ClassLoader classLoader) {
return classLoader::getResource;
}
static Classpath contextClasspath() {
return forClassLoader(Thread.currentThread().getContextClassLoader());
}
// </editor-fold>
}
@@ -168,7 +168,7 @@ class ClassTypeImpl implements JClassType {
}
@Override
@SuppressWarnings({"unchecked", "rawtypes"})
@SuppressWarnings({ "unchecked", "rawtypes" })
public List<JTypeMirror> getTypeArgs() {
return isGenericTypeDeclaration() ? (List) getFormalTypeParams() : typeArgs;
}
@@ -199,9 +199,7 @@ class ClassTypeImpl implements JClassType {
}
int expected = symbol.getTypeParameterCount();
if (typeArgs.size() != expected && !typeArgs.isEmpty()) {
throw invalidTypeArgs(symbol, typeArgs);
} else if (expected == 0) {
if (expected == 0 && typeArgs.isEmpty() && this.typeArgs.isEmpty()) {
return this; // non-generic
}
return new ClassTypeImpl(ts, symbol, CollectionUtil.defensiveUnmodifiableCopy(typeArgs), false);
@@ -344,19 +342,6 @@ class ClassTypeImpl implements JClassType {
checkUserEnclosingTypeIsOk(enclosing, symbol);
if (!typeArgsAreOk(symbol, typeArgs)) {
// fixme relax this
// This will throw if the symbol is unresolved and was
// resolved through AsmSymbolResolver (ie, a missing dependency
// in classpath, found in a signature of some ASM class symbol member).
// Currently the AST symbol impl tries to patch unresolved symbols by
// making the number of type params flexible. But this does not help
// the ASM implementation, and these errors are frequent if your classpath
// is missing something. We still want pmd to continue processing in this case.
// The best fix IMO is to admit malformed types provided they're unresolved.
// We'll have to abandon the assumption that every parameterized type for
// the same symbol has the same number of type params. And also, that the
// formal type parameter list always matches the type argument lists in length.
// Many places rely on this... For instance: TypeConversion#capture, TypeOps#isSameType, etc
throw invalidTypeArgs(symbol, typeArgs);
}
@@ -15,6 +15,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol;
import net.sourceforge.pmd.lang.java.symbols.JExecutableSymbol;
import net.sourceforge.pmd.lang.java.symbols.JMethodSymbol;
/**
* Represents class and interface types, including functional interface
@@ -128,31 +129,44 @@ public interface JClassType extends JTypeMirror {
/**
* A specific instantiation of the type variables in {@link #getFormalTypeParams()}.
* Note that the type arguments and formal type parameters may be mismatched in size,
* (only if the symbol is unresolved). In any case, no attempt is made to check that
* the type arguments conform to the bound on type parameters in methods like
* {@link #withTypeArguments(List)}, although this is taken into account during type
* inference.
*
* <p>If this type is not generic, or a raw type, returns an empty list.
* <p>If this is a {@linkplain #isGenericTypeDeclaration() generic type declaration},
* returns exactly the same list as {@link #getFormalTypeParams()}.
*
* @see #getFormalTypeParams()
*/
List<JTypeMirror> getTypeArgs();
/**
* Returns the list of type variables declared by the generic type declaration.
* These match {@link #getTypeArgs()} if this is a {@linkplain #isGenericTypeDeclaration() generic type
* declaration},
* which is distinct from a {@linkplain #isRaw() raw type}.
*
* <p>If this type is not generic, returns an empty list.
* <p>If this type is not generic, returns an empty list. Note that if the symbol
* is unresolved, it is considered non-generic. But it still may have type arguments.
*
* @see #getTypeArgs()
*/
List<JTypeVar> getFormalTypeParams();
/**
* Returns the substitution mapping the formal type parameters of all
* enclosing types to type arguments. If a type is raw, then its type
* enclosing types to their type arguments. If a type is raw, then its type
* parameters are not part of the returned mapping. Note, that this
* does not include type parameters of the supertypes.
*
* <p>If this type is erased, returns a substitution erasing all type
* parameters.
*
* <p>For instance, in the type {@code List<String>}, this is the substitution mapping
* the type parameter {@code T} of {@code interface List<T>} to {@code String}.
* It is suitable for use in e.g. {@link JMethodSymbol#getReturnType(Substitution)}.
*/
Substitution getTypeParamSubst();
@@ -174,8 +188,7 @@ public interface JClassType extends JTypeMirror {
* @throws IllegalArgumentException If the symbol is not a member type
* of this type (local/anon classes don't work)
* @throws IllegalArgumentException If the type arguments don't match the
* type parameters of the symbol (unless they're empty,
* in which case the selected type is raw)
* type parameters of the symbol (see {@link #withTypeArguments(List)})
* @throws IllegalArgumentException If this type is raw and the inner type is not,
* or this type is parameterized and the inner type is not
*/
@@ -248,7 +261,8 @@ public interface JClassType extends JTypeMirror {
*
* @throws IllegalArgumentException If the type argument list doesn't
* match the type parameters of this
* type in length
* type in length. If the symbol is unresolved,
* any number of type arguments is accepted.
* @throws IllegalArgumentException If any type of the list is null, or
* a primitive type
*/
@@ -186,7 +186,7 @@ public final class JIntersectionType implements JTypeMirror {
}
} else if (ci instanceof JClassType) {
// must be an interface, as per isExclusiveBlabla
assert ci.isInterface();
assert ci.isInterface() || TypeOps.hasUnresolvedSymbol(ci);
} else {
throw malformedIntersection(primary, flattened);
}
@@ -159,8 +159,6 @@ public final class TypeConversion {
List<JTypeMirror> typeArgs = type.getTypeArgs();
List<JTypeVar> typeParams = type.getFormalTypeParams();
assert typeParams.size() == typeArgs.size() : "Type is not well formed " + type + " (expects " + typeParams.size() + " params)";
// This is the algorithm described at https://docs.oracle.com/javase/specs/jls/se10/html/jls-5.html#jls-5.1.10
// Let G name a generic type declaration (§8.1.2, §9.1.2)
@@ -176,11 +174,14 @@ public final class TypeConversion {
List<JTypeMirror> freshVars = makeFreshVars(type);
// types may be non-well formed if the symbol is unresolved
// in this case the typeParams list is most likely empty
boolean wellFormed = typeParams.size() == freshVars.size();
// Map of Ai to Si, for the substitution
Substitution subst = Substitution.mapping(typeParams, freshVars);
Substitution subst = wellFormed ? Substitution.mapping(typeParams, freshVars) : Substitution.EMPTY;
for (int i = 0; i < typeArgs.size(); i++) {
JTypeVar param = typeParams.get(i); // Ai
JTypeMirror fresh = freshVars.get(i); // Si
JTypeMirror arg = typeArgs.get(i); // Ti
@@ -191,7 +192,7 @@ public final class TypeConversion {
JWildcardType w = (JWildcardType) arg; // Ti alias
TypeVarImpl.CapturedTypeVar freshVar = (TypeVarImpl.CapturedTypeVar) fresh; // Si alias
JTypeMirror prevUpper = param.getUpperBound(); // Ui
JTypeMirror prevUpper = wellFormed ? typeParams.get(i).getUpperBound() : ts.OBJECT; // Ui
JTypeMirror substituted = TypeOps.subst(prevUpper, subst);
if (w.isUnbounded()) {
@@ -387,11 +387,15 @@ public final class TypeOps {
} else if (isSpecialUnresolved(t)) {
// error type or unresolved type
return Convertibility.SUBTYPING;
} else if (hasUnresolvedSymbol(t)) {
} else if (hasUnresolvedSymbol(t) && t instanceof JClassType) {
// This also considers types with an unresolved symbol
// subtypes of (nearly) anything. This allows them to
// pass bound checks on type variables.
return Convertibility.subtypeIf(s instanceof JClassType); // excludes array or so
if (Objects.equals(t.getSymbol(), s.getSymbol())) {
return typeArgsAreContained((JClassType) t, (JClassType) s);
} else {
return Convertibility.subtypeIf(s instanceof JClassType); // excludes array or so
}
} else if (s instanceof JIntersectionType) { // TODO test intersection with tvars & arrays
// If S is an intersection, then T must conform to *all* bounds of S
// Symmetrically, if T is an intersection, T <: S requires only that
@@ -692,6 +696,11 @@ public final class TypeOps {
: Convertibility.UNCHECKED_WARNING;
}
if (targs.size() != sargs.size()) {
// types are not well-formed
return Convertibility.NEVER;
}
Convertibility result = Convertibility.SUBTYPING;
for (int i = 0; i < targs.size(); i++) {
Convertibility sub = typeArgContains(sargs.get(i), targs.get(i));
@@ -1360,8 +1369,8 @@ public final class TypeOps {
* this does not check the static modifier, and tests for hiding
* if the method is static.
*
* @param m Method to test
* @param origin Site of the potential override
* @param m Method to test
* @param origin Site of the potential override
*/
public static boolean isOverridableIn(JExecutableSymbol m, JTypeDeclSymbol origin) {
if (m instanceof JConstructorSymbol) {
@@ -1664,11 +1673,12 @@ public final class TypeOps {
// Notice that this loop needs a well-behaved subtyping relation,
// i.e. antisymmetric: A <: B && A != B implies not(B <: A)
// This is not the case if we include unchecked conversion in there.
// This is not the case if we include unchecked conversion in there,
// or special provisions for unresolved types.
vLoop:
for (JTypeMirror v : set) {
for (JTypeMirror w : set) {
if (!w.equals(v) && isSubtypePure(w, v).bySubtyping()) {
if (!w.equals(v) && !hasUnresolvedSymbol(w) && isSubtypePure(w, v).bySubtyping()) {
continue vLoop;
}
}
@@ -1936,9 +1946,14 @@ public final class TypeOps {
return t == ts.UNKNOWN || t == ts.ERROR;
}
/**
* Return true if the argument is a {@link JClassType} with
* {@linkplain JClassSymbol#isUnresolved() an unresolved symbol} or
* a {@link JArrayType} whose element type matches the first criterion.
*/
public static boolean hasUnresolvedSymbol(@Nullable JTypeMirror t) {
if (!(t instanceof JClassType)) {
return false;
return t instanceof JArrayType && hasUnresolvedSymbol(((JArrayType) t).getElementType());
}
return t.getSymbol() != null && t.getSymbol().isUnresolved();
}
@@ -33,6 +33,7 @@ import net.sourceforge.pmd.lang.java.symbols.JTypeParameterSymbol;
import net.sourceforge.pmd.lang.java.symbols.SymbolResolver;
import net.sourceforge.pmd.lang.java.symbols.internal.UnresolvedClassStore;
import net.sourceforge.pmd.lang.java.symbols.internal.asm.AsmSymbolResolver;
import net.sourceforge.pmd.lang.java.symbols.internal.asm.Classpath;
import net.sourceforge.pmd.lang.java.types.BasePrimitiveSymbol.RealPrimitiveSymbol;
import net.sourceforge.pmd.lang.java.types.BasePrimitiveSymbol.VoidSymbol;
import net.sourceforge.pmd.lang.java.types.JPrimitiveType.PrimitiveTypeKind;
@@ -53,7 +54,7 @@ import net.sourceforge.pmd.util.CollectionUtil;
* <p>The lifetime of a type system is the analysis: it is shared by
* all compilation units.
* TODO this is hacked together by comparing the ClassLoader, but this
* should be in the language instance
* should be in the language instance
*
* <p>Nodes have a reference to the type system they were created for:
* {@link JavaNode#getTypeSystem()}.
@@ -179,8 +180,20 @@ public final class TypeSystem {
* to populate the fields of the new type
* system
*/
public TypeSystem(ClassLoader bootstrapResourceLoader) {
this(ts -> new AsmSymbolResolver(ts, bootstrapResourceLoader));
public static TypeSystem usingClassLoaderClasspath(ClassLoader bootstrapResourceLoader) {
return usingClasspath(Classpath.forClassLoader(bootstrapResourceLoader));
}
/**
* Builds a new type system. Its public fields will be initialized
* with fresh types, unrelated to other types.
*
* @param bootstrapResourceLoader Classpath used to resolve class files
* to populate the fields of the new type
* system
*/
public static TypeSystem usingClasspath(Classpath bootstrapResourceLoader) {
return new TypeSystem(ts -> new AsmSymbolResolver(ts, bootstrapResourceLoader));
}
/**
@@ -482,17 +495,12 @@ public final class TypeSystem {
return typeOf(klass, false);
}
// TODO spec
// - should be equivalent to rawType(klass).withTypeArguments(typeArgs)
// - should not accept malformed types, esp. those where there is an enclosing type
// - test: should not recreate OBJECT
/**
* Produce a parameterized type with the given symbol and type arguments.
* The type argument list must match the declared formal type parameters in
* length. Non-generic symbols are accepted by this method, provided the
* argument list is empty.
* argument list is empty. If the symbol is unresolved, any type argument
* list is accepted.
*
* <p>This method is equivalent to {@code rawType(klass).withTypeArguments(typeArgs)},
* but that code would require a cast.
@@ -724,6 +732,7 @@ public final class TypeSystem {
}
private static final class NullType implements JTypeMirror {
private final TypeSystem ts;
NullType(TypeSystem ts) {
@@ -0,0 +1,22 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package javasymbols.testdata;
import net.sourceforge.pmd.lang.java.symbols.internal.asm.Classpath;
/**
* For this test we exclude SuperItf.class from the {@link Classpath}
* to mimic an incomplete classpath.
*
* @see net.sourceforge.pmd.lang.java.symbols.internal.asm.BrokenClasspathTest
*/
public class BrokenGeneric<T0, T1>
extends SuperKlass<T0, T0>
implements SuperItf<T1, T1> {
}
class SuperKlass<A, B> { }
interface SuperItf<A, B> { }
@@ -37,7 +37,7 @@ public class JavaParsingHelper extends BaseParsingHelper<JavaParsingHelper, ASTC
* default options of JavaParsingHelper. This allows constants like
* the null type to be compared.
*/
public static final TypeSystem TEST_TYPE_SYSTEM = new TypeSystem(JavaParsingHelper.class.getClassLoader());
public static final TypeSystem TEST_TYPE_SYSTEM = TypeSystem.usingClassLoaderClasspath(JavaParsingHelper.class.getClassLoader());
/** This just runs the parser and no processing stages. */
public static final JavaParsingHelper JUST_PARSE = new JavaParsingHelper(Params.getDefaultNoProcess(), SemanticErrorReporter.noop(), TEST_TYPE_SYSTEM, TypeInferenceLogger.noop());
@@ -34,10 +34,11 @@ class AsmLoaderTest : FunSpec({
// method reference with static ctdecl & zero formal parameters (asInstanceMethod)
val contextClasspath = Classpath { Thread.currentThread().contextClassLoader.getResource(it) }
test("First ever ASM test") {
val symLoader = AsmSymbolResolver(testTypeSystem, Thread.currentThread().contextClassLoader)
val symLoader = AsmSymbolResolver(testTypeSystem, contextClasspath)
val loaded = symLoader.resolveClassFromBinaryName("javasymbols.testdata.StaticNameCollision")!!
@@ -49,7 +50,7 @@ class AsmLoaderTest : FunSpec({
}
val ts = testTypeSystem
val symLoader = AsmSymbolResolver(ts, Thread.currentThread().contextClassLoader)
val symLoader = AsmSymbolResolver(ts, contextClasspath)
test("Generic class") {
@@ -0,0 +1,78 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.java.symbols.internal.asm
import io.kotest.assertions.fail
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import net.sourceforge.pmd.lang.ast.test.shouldBe
import net.sourceforge.pmd.lang.java.types.JClassType
import net.sourceforge.pmd.lang.java.types.TypeOps
import net.sourceforge.pmd.lang.java.types.TypeSystem
import net.sourceforge.pmd.lang.java.types.testTypeSystem
class BrokenClasspathTest : FunSpec({
val rootCp = Classpath.contextClasspath()
val brokenCp = rootCp.exclude(setOf("javasymbols/testdata/SuperItf.class"))
test("Test classpath setup ") {
rootCp.findResource("javasymbols/testdata/BrokenGeneric.class") shouldNotBe null
rootCp.findResource("javasymbols/testdata/SuperItf.class") shouldNotBe null
rootCp.findResource("javasymbols/testdata/SuperKlass.class") shouldNotBe null
// this one is null
brokenCp.findResource("javasymbols/testdata/SuperItf.class") shouldBe null
brokenCp.findResource("javasymbols/testdata/BrokenGeneric.class") shouldNotBe null
brokenCp.findResource("javasymbols/testdata/SuperKlass.class") shouldNotBe null
}
test("Test load subclass (symbol only)") {
val resolver = AsmSymbolResolver(testTypeSystem, brokenCp)
val found = resolver.resolveClassFromBinaryName("javasymbols.testdata.BrokenGeneric")
?: fail("not found")
found.superclass!!::getBinaryName shouldBe "javasymbols.testdata.SuperKlass"
found.superInterfaces!!.map { it.binaryName } shouldBe listOf("javasymbols.testdata.SuperItf")
withClue("Isn't resolved") {
found.superInterfaces[0]::isUnresolved shouldBe true
}
}
test("Test load from typesystem") {
val ts = TypeSystem.usingClasspath(brokenCp)
val subclassSym = ts.getClassSymbol("javasymbols.testdata.BrokenGeneric")!!
val unresolvedItfSym = subclassSym.superInterfaces[0]
unresolvedItfSym::isUnresolved shouldBe true
// since we're loading things lazily this type hasn't tried to populate its superinterfaces
val superItfType = ts.declaration(unresolvedItfSym) as JClassType
val subclassType = ts.declaration(subclassSym) as JClassType
val (tvarC, tvarD) = subclassType.formalTypeParams
// and now since the super interface *type* is parameterized, we'll try to create SuperItf<D,D>
// Except SuperItf is unresolved.
val expected = ts.parameterise(unresolvedItfSym, listOf(tvarD, tvarD))
expected shouldBe superItfType.withTypeArguments(listOf(tvarD, tvarD))
subclassType.superInterfaces[0] shouldBe expected
subclassType.isConvertibleTo(expected) shouldBe TypeOps.Convertibility.SUBTYPING
}
})
fun TypeSystem.createUnresolvedAsmSymbol(binaryName: String) =
AsmSymbolResolver(this, Classpath.contextClasspath())
.resolveFromInternalNameCannotFail(binaryName.replace('.', '/'))
@@ -7,6 +7,8 @@ package net.sourceforge.pmd.lang.java.types
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol
import net.sourceforge.pmd.lang.java.symbols.internal.asm.createUnresolvedAsmSymbol
import net.sourceforge.pmd.lang.java.types.TypeConversion.*
/**
@@ -55,6 +57,19 @@ class CaptureTest : FunSpec({
}
}
test("Capture of malformed types") {
val sym = ts.createUnresolvedAsmSymbol("does.not.Exist") as JClassSymbol
val matcher = captureMatcher(`?` extends t_String).also {
capture(sym[t_String, `?` extends t_String]) shouldBe sym[t_String, it]
}
matcher.also {
it.isCaptured shouldBe true
it.isCaptureOf(`?` extends t_String) shouldBe true
}
}
}
}
@@ -0,0 +1,52 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.java.types
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec
import io.kotest.inspectors.forNone
import io.kotest.matchers.shouldBe
import io.kotest.property.Exhaustive
import io.kotest.property.checkAll
import io.kotest.property.exhaustive.ints
import io.kotest.property.forAll
import net.sourceforge.pmd.lang.ast.test.shouldBeA
import net.sourceforge.pmd.lang.java.ast.ParserTestCtx
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol
import net.sourceforge.pmd.lang.java.symbols.internal.UnresolvedClassStore
import net.sourceforge.pmd.lang.java.symbols.internal.asm.createUnresolvedAsmSymbol
import net.sourceforge.pmd.lang.java.types.TypeConversion.*
import net.sourceforge.pmd.lang.java.types.TypeOps.Convertibility.*
import net.sourceforge.pmd.lang.java.types.testdata.ComparableList
import net.sourceforge.pmd.lang.java.types.testdata.SomeEnum
import kotlin.test.assertTrue
/**
* @author Clément Fournier
*/
class ClassTypeImplTest : FunSpec({
val ts = testTypeSystem
with(TypeDslOf(ts)) {
with(gen) {
test("Test repeated withTypeArguments on unresolved type") {
val sym = ts.createUnresolvedAsmSymbol("does.not.Exist") as JClassSymbol
val t = ts.declaration(sym) as JClassType
t.withTypeArguments(listOf(t_String)).typeArgs shouldBe listOf(t_String)
t.withTypeArguments(listOf(t_String))
.withTypeArguments(emptyList()).typeArgs shouldBe emptyList()
}
}
}
})
@@ -10,8 +10,8 @@ import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.shouldBe
import io.kotest.property.checkAll
import io.kotest.property.forAll
import net.sourceforge.pmd.lang.ast.test.shouldBeA
import net.sourceforge.pmd.lang.java.symbols.internal.asm.createUnresolvedAsmSymbol
/**
* Tests "the greatest lower bound" (glb).
@@ -26,8 +26,8 @@ class GlbTest : FunSpec({
test("Test intersection minimization") {
forAll(ts.subtypesArb()) { (t, s) ->
glb(t, s) == t
checkAll(ts.subtypesArb()) { (t, s) ->
glb(t, s) shouldBe t
}
// in particular
@@ -112,6 +112,20 @@ class GlbTest : FunSpec({
it.components.shouldContainExactly(`t_Enum{JPrimitiveType}`, `t_List{String}`, `t_List{? extends Number}`)
}
}
test("Test GLB with unresolved things") {
val tA = ts.declaration(ts.createUnresolvedAsmSymbol("a.A"))
val tB = ts.declaration(ts.createUnresolvedAsmSymbol("a.B"))
tA shouldBeSubtypeOf tB
tB shouldBeSubtypeOf tA
TypeOps.mostSpecific(setOf(tA, tB)) shouldBe setOf(tA, tB)
glb(tA, tB).shouldBeA<JIntersectionType> {
it.components.shouldContainExactly(tA, tB)
}
}
}
}
@@ -15,7 +15,9 @@ import io.kotest.property.exhaustive.ints
import io.kotest.property.forAll
import net.sourceforge.pmd.lang.ast.test.shouldBeA
import net.sourceforge.pmd.lang.java.ast.ParserTestCtx
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol
import net.sourceforge.pmd.lang.java.symbols.internal.UnresolvedClassStore
import net.sourceforge.pmd.lang.java.symbols.internal.asm.createUnresolvedAsmSymbol
import net.sourceforge.pmd.lang.java.types.TypeConversion.*
import net.sourceforge.pmd.lang.java.types.TypeOps.Convertibility.*
import net.sourceforge.pmd.lang.java.types.testdata.ComparableList
@@ -209,7 +211,7 @@ class SubtypingTest : FunSpec({
test("Test wildcard subtyping (property-based)") {
checkAll(ts.subtypesArb(false)) { (t, s) ->
checkAll(ts.subtypesArb()) { (t, s) ->
t_List[t] shouldBeSubtypeOf t_List[`?` extends s]
t_List[s] shouldBeSubtypeOf t_List[`?` `super` t]
}
@@ -260,7 +262,7 @@ class SubtypingTest : FunSpec({
test("Unresolved symbol is compatible with any class/interface") {
val t = ts.declaration(UnresolvedClassStore(ts).makeUnresolvedReference("obj.foo", 0))
.shouldBeUnresolvedClass("obj.foo")
.shouldBeUnresolvedClass("obj.foo")
checkAll(ts.primitiveGen) { s ->
t shouldNotBeSubtypeOf s
@@ -294,7 +296,7 @@ class SubtypingTest : FunSpec({
test("Captured subtyping wild vs wild") {
checkAll(ts.subtypesArb(unchecked = false)) { (t, s) ->
checkAll(ts.subtypesArb()) { (t, s) ->
// println("$t <: $s")
capture(t_List[`?` extends t]) shouldSubtypeNoCapture t_List[`?` extends s]
@@ -315,6 +317,13 @@ class SubtypingTest : FunSpec({
t_List[`?` extends t] shouldNotSubtypeNoCapture t_List[`?` extends someCapture]
}
}
test("Test non well-formed types") {
val sym = ts.createUnresolvedAsmSymbol("does.not.Exist") as JClassSymbol
sym[t_String, t_String] shouldBeUnrelatedTo sym[t_String]
sym[t_String] shouldBeSubtypeOf sym[t_String]
sym[t_String] shouldBeSubtypeOf sym[`?` extends t_String] // containment
}
}
}
@@ -10,6 +10,8 @@ import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.property.checkAll
import io.kotest.property.forAll
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol
import net.sourceforge.pmd.lang.java.symbols.internal.asm.createUnresolvedAsmSymbol
import net.sourceforge.pmd.lang.java.symbols.internal.forAllEqual
/**
@@ -78,6 +80,20 @@ class TypeEqualityTest : FunSpec({
}
}
}
test("Test non well-formed types") {
val sym = ts.createUnresolvedAsmSymbol("does.not.Exist") as JClassSymbol
// not equal
sym[t_String, t_String] shouldNotBe sym[t_String]
sym[t_String] shouldNotBe sym[t_String, t_String]
sym[t_Integer] shouldNotBe sym[t_String]
// equal
sym[t_String, t_String] shouldBe sym[t_String, t_String]
sym[t_String] shouldBe sym[t_String]
sym[t_String, t_Integer] shouldBe sym[t_String, t_Integer]
}
}
}
@@ -17,6 +17,7 @@ import net.sourceforge.pmd.lang.java.ast.ASTTypeParameter
import net.sourceforge.pmd.lang.java.ast.ASTTypeParameters
import net.sourceforge.pmd.lang.java.ast.ParserTestCtx
import net.sourceforge.pmd.lang.java.symbols.internal.TestClassesGen
import net.sourceforge.pmd.lang.java.symbols.internal.asm.createUnresolvedAsmSymbol
import javax.lang.model.type.TypeMirror
import kotlin.streams.toList
@@ -31,9 +32,13 @@ val TypeSystem.allTypesGen: Arb<JTypeMirror>
}
}
fun TypeSystem.subtypesArb(unchecked: Boolean = false) =
/**
* Only well-behaved subtypes
*/
fun TypeSystem.subtypesArb() =
Arb.pair(refTypeGen, refTypeGen)
.filter { (t, s) -> t.isConvertibleTo(s).bySubtyping() }
.filter { (t, s) -> !TypeOps.hasUnresolvedSymbol(t) && !TypeOps.hasUnresolvedSymbol(s) }
infix fun Boolean.implies(v: () -> Boolean): Boolean = !this || v()
@@ -47,6 +52,9 @@ class RefTypeGenArb(val ts: TypeSystem) : Arb<JTypeMirror>() {
private fun generateTypes(rs : RandomSource) : List<JTypeMirror> {
with(TypeDslOf(ts).gen) {
val unresolved1 = ts.createUnresolvedAsmSymbol("some.fake.Symbol")
val unresolved2 = ts.createUnresolvedAsmSymbol("another.fake.Symbol")
val pool: List<JTypeMirror> = listOf(
`t_List{String}`,
t_Enum,
@@ -61,7 +69,10 @@ class RefTypeGenArb(val ts: TypeSystem) : Arb<JTypeMirror>() {
t_CharSequence,
t_StringBuilder,
t_MapEntry,
`t_Array{Object}`
`t_Array{Object}`,
ts.declaration(unresolved1)[t_String],
ts.declaration(unresolved2)[t_Integer, `?` extends `t_List{? extends Number}`],
ts.declaration(unresolved2)
).flatMap {
it.superTypeSet.toList() + it + it.erasure + it.toArray()
}
@@ -8,9 +8,9 @@ import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
import io.kotest.property.Arb
import io.kotest.property.arbitrary.map
import io.kotest.property.arbitrary.shuffle
import io.kotest.property.checkAll
import net.sourceforge.pmd.lang.java.symbols.internal.asm.createUnresolvedAsmSymbol
/**
* @author Clément Fournier
@@ -31,7 +31,7 @@ class TypeOpsTest : FunSpec({
test("Test most specific") {
checkAll(ts.subtypesArb(false)) { (t, s) ->
checkAll(ts.subtypesArb()) { (t, s) ->
TypeOps.mostSpecific(setOf(t, s)).shouldContainExactly(t)
}
}
@@ -43,6 +43,16 @@ class TypeOpsTest : FunSpec({
output = listOf(t_AbstractList))
}
test("Test most specific of unresolved types") {
val tA = ts.declaration(ts.createUnresolvedAsmSymbol("a.A"))
val tB = ts.declaration(ts.createUnresolvedAsmSymbol("a.B"))
checkMostSpecific(
input = listOf(tA, tB),
output = listOf(tA, tB)
)
}
test("Test most specific unchecked") {
@@ -10,6 +10,7 @@ import io.kotest.matchers.shouldBe
import net.sourceforge.pmd.lang.java.ast.*
import net.sourceforge.pmd.lang.java.types.*
import net.sourceforge.pmd.util.OptionalBool
import org.intellij.lang.annotations.Language
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@@ -21,8 +22,7 @@ class OverridingTest : ProcessorTestSpec({
fun overridingSetup(name: String, mod: String) {
parserTest(name) {
val acu = parser.parse(
"""
val acu = parser.parse("""
class Scratch {
$mod void m(Throwable t) { }
}
@@ -85,8 +85,7 @@ class Other extends Scratch {
parserTest("Both inherited and visible from outer class") {
val acu = parser.parse(
"""
val acu = parser.parse("""
class Scratch {
static void m(Throwable t) { }
@@ -94,7 +93,7 @@ class Scratch {
static void m(Throwable t) { }
}
}
""".trimIndent()
""".trimIndent()
)
val (scratchM, otherM) = acu.methodDeclarations().toList { it.genericSignature }
@@ -118,8 +117,7 @@ class Scratch {
parserTest("Primitive signatures do not merge") {
val acu = parser.parse(
"""
val acu = parser.parse("""
class Scratch {
void m(long t) { }
@@ -127,7 +125,7 @@ class Scratch {
void m(int t) { }
}
}
""".trimIndent()
""".trimIndent()
)
val (scratchM, otherM) = acu.methodDeclarations().toList { it.genericSignature }
@@ -139,8 +137,7 @@ class Scratch {
}
parserTest("Primitive signatures do not merge 2") {
val acu = parser.parse(
"""
val acu = parser.parse("""
class Scratch {
void m(int t) { }
@@ -148,7 +145,7 @@ class Scratch {
void m(long t) { }
}
}
""".trimIndent()
""".trimIndent()
)
val (scratchM, otherM) = acu.methodDeclarations().toList { it.genericSignature }
@@ -162,8 +159,7 @@ class Scratch {
parserTest("Static generic method") {
val acu = parser.parse(
"""
val acu = parser.parse("""
class Sup {
static <E> E m(F<? extends E> e) { return null; }
}
@@ -177,7 +173,7 @@ class F<G> {
Sub.m(); // should be Sub::m, no ambiguity
}
}
""".trimIndent()
""".trimIndent()
)
val (supE, subE) = acu.declaredMethodSignatures()
@@ -190,15 +186,14 @@ class F<G> {
parserTest("Static method with different bound") {
val (acu, spy) = parser.parseWithTypeInferenceSpy(
"""
val (acu, spy) = parser.parseWithTypeInferenceSpy("""
import java.util.List;
class Sup {
static <E> E m(F<? extends E> e) { return null; }
static <E, _X> E m(F<? extends E> e) { return null; }
}
class Sub extends Sup {
static <S extends List<S>> F<S> m(F<? extends S> e) { return null; }
static <S extends List<TP>, TP> F<S> m(F<? extends S> e) { return null; }
}
class F<G> {
@@ -209,7 +204,7 @@ class F<G> {
Sub.m(new F<List<G>>());
}
}
""".trimIndent()
""".trimIndent()
)
val (supE, subE) = acu.declaredMethodSignatures()
@@ -218,7 +213,7 @@ class F<G> {
// their type parameters have a different bound
// technically this is a compile-time error:
// both methods have the same erasure but neither hides the other
assertFalse("Methods are override-equivalent") {
assertFalse("Methods should not override each other\n\t$subE\n\t$supE") {
TypeOps.overrides(subE, supE, subE.declaringType)
}
@@ -230,11 +225,9 @@ class F<G> {
parserTest("Static method of interface is not inherited!") {
parserTest("Static method of interface is not inherited!") {
val (acu, spy) = parser.parseWithTypeInferenceSpy(
"""
val (acu, spy) = parser.parseWithTypeInferenceSpy("""
interface List<T> {}
interface Sup {
@@ -252,7 +245,7 @@ class F<G> implements List<F<G>> {
Sub.m(new F<F<List<G>>>());
}
}
""".trimIndent()
""".trimIndent()
)
val (supE, subE) = acu.declaredMethodSignatures()
@@ -270,11 +263,9 @@ class F<G> implements List<F<G>> {
parserTest("Static method of interface is not inherited in subinterfaces either") {
parserTest("Static method of interface is not inherited in subinterfaces either") {
val (acu, spy) = parser.parseWithTypeInferenceSpy(
"""
val (acu, spy) = parser.parseWithTypeInferenceSpy("""
interface List<T> {}
interface Sup {
@@ -292,7 +283,7 @@ class F<G> implements List<F<G>> {
Sub.m(new F<F<List<G>>>());
}
}
""".trimIndent()
""".trimIndent()
)
val (supE, subE) = acu.declaredMethodSignatures()
@@ -311,8 +302,7 @@ class F<G> implements List<F<G>> {
parserTest("Private method shadowed in inner class") {
val acu = parser.parse(
"""
val acu = parser.parse("""
class Scratch {
private void m(Throwable t) { } // private methods are not overridden
@@ -320,7 +310,7 @@ class Scratch {
private void m(Throwable t) { }
}
}
""".trimIndent()
""".trimIndent()
)
val (scratchM, otherM) = acu.methodDeclarations().toList { it.genericSignature }
@@ -345,8 +335,7 @@ class Scratch {
}
parserTest("Merged abstract signature in class") {
val acu = parser.parse(
"""
val acu = parser.parse("""
class Scratch {
static void m(Throwable t) { }
@@ -354,7 +343,7 @@ class Scratch {
static void m(Throwable t) { }
}
}
""".trimIndent()
""".trimIndent()
)
val (scratchM, otherM) = acu.methodDeclarations().toList { it.genericSignature }