diff --git a/README.md b/README.md index 04071c0..7c09913 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,10 @@ This tool can detect _some_ binary incompatibilities at build time. Specifically * Missing classes * Duplicate classes * Classes with missing superclasses and interfaces +* Classes extending final classes * Missing methods and fields * Static methods and fields accessed non-statically and vice-versa -* Inaccessible methods and fields (partially) +* Inaccessible methods and fields ## Application vs platform classes diff --git a/animal-sniffer-format/src/main/kotlin/com/toasttab/expediter/sniffer/AnimalSnifferParser.kt b/animal-sniffer-format/src/main/kotlin/com/toasttab/expediter/sniffer/AnimalSnifferParser.kt index cc2cfb7..1a9668a 100644 --- a/animal-sniffer-format/src/main/kotlin/com/toasttab/expediter/sniffer/AnimalSnifferParser.kt +++ b/animal-sniffer-format/src/main/kotlin/com/toasttab/expediter/sniffer/AnimalSnifferParser.kt @@ -20,6 +20,7 @@ import com.toasttab.expediter.types.AccessProtection import com.toasttab.expediter.types.MemberDescriptor import com.toasttab.expediter.types.MemberSymbolicReference import com.toasttab.expediter.types.TypeDescriptor +import com.toasttab.expediter.types.TypeExtensibility import com.toasttab.expediter.types.TypeFlavor import java.io.InputStream import java.util.zip.GZIPInputStream @@ -61,7 +62,8 @@ object AnimalSnifferParser { it.superInterfaces, members, AccessProtection.UNKNOWN, - TypeFlavor.UNKNOWN + TypeFlavor.UNKNOWN, + TypeExtensibility.UNKNOWN ) } } diff --git a/core/src/main/kotlin/com/toasttab/expediter/AccessCheck.kt b/core/src/main/kotlin/com/toasttab/expediter/AccessCheck.kt index d95ba47..f1e03da 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/AccessCheck.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/AccessCheck.kt @@ -1,21 +1,26 @@ package com.toasttab.expediter import com.toasttab.expediter.types.AccessProtection +import com.toasttab.expediter.types.ApplicationTypeWithResolvedHierarchy import com.toasttab.expediter.types.MemberDescriptor import com.toasttab.expediter.types.MemberType +import com.toasttab.expediter.types.ResolvedTypeHierarchy import com.toasttab.expediter.types.TypeDescriptor object AccessCheck { - fun allowedAccess(caller: TypeDescriptor, target: TypeDescriptor, member: MemberDescriptor): Boolean { + fun allowedAccess(caller: ApplicationTypeWithResolvedHierarchy, target: TypeDescriptor, member: MemberDescriptor): Boolean { return if (member.protection == AccessProtection.PRIVATE) { - return caller.sameClassAs(target) + caller.name == target.name } else if (member.protection == AccessProtection.PACKAGE_PRIVATE || target.protection == AccessProtection.PACKAGE_PRIVATE) { - return caller.samePackageAs(target) + samePackage(caller.name, target.name) + } else if (member.protection == AccessProtection.PROTECTED) { + samePackage(caller.name, target.name) || + // if we can't resolve all supertypes, we can't say for sure that access is not allowed + caller.hierarchy !is ResolvedTypeHierarchy.CompleteTypeHierarchy || + caller.hierarchy.allTypes.any { it.name == target.name } } else { true - } // TODO: add other checks + } } - - private fun TypeDescriptor.sameClassAs(other: TypeDescriptor) = name == other.name - private fun TypeDescriptor.samePackageAs(other: TypeDescriptor) = name.substringBeforeLast('/') == other.name.substringBeforeLast('/') + private fun samePackage(a: String, b: String) = a.substringBeforeLast('/') == b.substringBeforeLast('/') } diff --git a/core/src/main/kotlin/com/toasttab/expediter/AttributeParser.kt b/core/src/main/kotlin/com/toasttab/expediter/AttributeParser.kt index 2df2112..748c4e0 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/AttributeParser.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/AttributeParser.kt @@ -17,10 +17,12 @@ package com.toasttab.expediter import com.toasttab.expediter.types.AccessDeclaration import com.toasttab.expediter.types.AccessProtection +import com.toasttab.expediter.types.TypeExtensibility +import com.toasttab.expediter.types.TypeFlavor import org.objectweb.asm.Opcodes object AttributeParser { - fun isInterface(access: Int) = (access and Opcodes.ACC_INTERFACE) != 0 + fun flavor(access: Int) = if (access and Opcodes.ACC_INTERFACE != 0) TypeFlavor.INTERFACE else TypeFlavor.CLASS fun protection(access: Int) = if (access and Opcodes.ACC_PRIVATE != 0) { @@ -39,4 +41,11 @@ object AttributeParser { } else { AccessDeclaration.INSTANCE } + + fun extensibility(access: Int) = + if (access and Opcodes.ACC_FINAL != 0) { + TypeExtensibility.FINAL + } else { + TypeExtensibility.NOT_FINAL + } } diff --git a/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt b/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt index 3c03b04..6f33609 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt @@ -18,50 +18,98 @@ package com.toasttab.expediter import com.toasttab.expediter.ignore.Ignore import com.toasttab.expediter.issue.Issue import com.toasttab.expediter.types.AccessDeclaration +import com.toasttab.expediter.types.ApplicationTypeWithResolvedHierarchy import com.toasttab.expediter.types.InspectedTypes import com.toasttab.expediter.types.MemberAccess import com.toasttab.expediter.types.MemberDescriptor -import com.toasttab.expediter.types.MemberSymbolicReference import com.toasttab.expediter.types.MemberType +import com.toasttab.expediter.types.MethodAccessType +import com.toasttab.expediter.types.OptionalResolvedTypeHierarchy import com.toasttab.expediter.types.PlatformTypeProvider +import com.toasttab.expediter.types.ResolvedTypeHierarchy import com.toasttab.expediter.types.TypeDescriptor -import com.toasttab.expediter.types.TypeHierarchy +import com.toasttab.expediter.types.TypeExtensibility +import com.toasttab.expediter.types.TypeFlavor class Expediter( private val ignore: Ignore, private val applicationTypesProvider: ApplicationTypesProvider, private val platformTypeProvider: PlatformTypeProvider ) { - fun findIssues(): Set { - val inspectedTypes = InspectedTypes(applicationTypesProvider.types(), platformTypeProvider) + private val inspectedTypes: InspectedTypes by lazy { + InspectedTypes(applicationTypesProvider.types(), platformTypeProvider) + } - return ( - inspectedTypes.classes.flatMap { cls -> - cls.refs.mapNotNull { access -> - if (!ignore.ignore(cls.type.name, access.targetType, access.ref)) { - findIssue(cls.type, access, inspectedTypes.hierarchy(access)) - } else { - null - } + private fun duplicates() = inspectedTypes.duplicateTypes.filter { + !ignore.ignore(null, it.target, null) + } + + private fun findIssues(appTypeWithHierarchy: ApplicationTypeWithResolvedHierarchy): Collection { + val issues = mutableListOf() + + when (appTypeWithHierarchy.hierarchy) { + is ResolvedTypeHierarchy.IncompleteTypeHierarchy -> { + issues.add( + Issue.MissingApplicationSuperType( + appTypeWithHierarchy.name, + appTypeWithHierarchy.hierarchy.missingType.map { it.name }.toSet() + ) + ) + } + + is ResolvedTypeHierarchy.CompleteTypeHierarchy -> { + val finalSupertypes = + appTypeWithHierarchy.hierarchy.superTypes.filter { it.extensibility == TypeExtensibility.FINAL } + .toList() + if (finalSupertypes.isNotEmpty()) { + issues.add( + Issue.FinalApplicationSuperType( + appTypeWithHierarchy.name, + finalSupertypes.map { it.name }.toSet() + ) + ) } - } + inspectedTypes.duplicateTypes.filter { - !ignore.ignore(null, it.target, null) } + } + + issues.addAll( + appTypeWithHierarchy.appType.refs.mapNotNull { access -> + if (!ignore.ignore(appTypeWithHierarchy.name, access.targetType, access.ref)) { + findIssue(appTypeWithHierarchy, access, inspectedTypes.resolveHierarchy(access.targetType)) + } else { + null + } + } + ) + + return issues + } + + fun findIssues(): Set { + return ( + inspectedTypes.classes.flatMap { appType -> + findIssues( + ApplicationTypeWithResolvedHierarchy( + appType, + inspectedTypes.resolveHierarchy(appType.type) + ) + ) + } + duplicates() ).toSet() } } -fun findIssue(type: TypeDescriptor, access: MemberAccess, chain: TypeHierarchy): Issue? { +fun findIssue(type: ApplicationTypeWithResolvedHierarchy, access: MemberAccess, chain: OptionalResolvedTypeHierarchy): Issue? { return when (chain) { - is TypeHierarchy.IncompleteTypeHierarchy -> Issue.MissingSuperType( + is ResolvedTypeHierarchy.IncompleteTypeHierarchy -> Issue.MissingSuperType( type.name, access.targetType, chain.missingType.map { it.name }.toSet() ) - is TypeHierarchy.NoType -> Issue.MissingType(type.name, access.targetType) - is TypeHierarchy.CompleteTypeHierarchy -> { - val member = chain.findMember(access.ref) + is OptionalResolvedTypeHierarchy.NoType -> Issue.MissingType(type.name, access.targetType) + is ResolvedTypeHierarchy.CompleteTypeHierarchy -> { + val member = chain.resolveMember(access) if (member == null) { Issue.MissingMember(type.name, access) @@ -86,10 +134,28 @@ private class MemberWithDeclaringType ( val declaringType: TypeDescriptor ) -private fun TypeHierarchy.CompleteTypeHierarchy.findMember(ref: MemberSymbolicReference): MemberWithDeclaringType? { - for (cls in classes) { +private fun ResolvedTypeHierarchy.CompleteTypeHierarchy.filterToAccessType(access: MemberAccess): Sequence { + return if (access !is MemberAccess.MethodAccess || + access.accessType == MethodAccessType.VIRTUAL || + access.accessType == MethodAccessType.STATIC || + (access.accessType == MethodAccessType.SPECIAL && !access.ref.isConstructor()) + ) { + // fields and methods, except for constructors and methods invoked via invokeinterface + // can be declared on any type in the hierarchy + allTypes + } else if (access.accessType == MethodAccessType.INTERFACE) { + // methods invoked via invokeinterface must be declared on an interface + allTypes.filter { it.flavor != TypeFlavor.CLASS } + } else { + // constructors must always be declared by the type being constructed + sequenceOf(type) + } +} + +private fun ResolvedTypeHierarchy.CompleteTypeHierarchy.resolveMember(access: MemberAccess): MemberWithDeclaringType? { + for (cls in filterToAccessType(access)) { for (m in cls.members) { - if (ref == m.ref) { + if (access.ref == m.ref) { return MemberWithDeclaringType(m as MemberDescriptor, cls) } } diff --git a/core/src/main/kotlin/com/toasttab/expediter/TypeParsers.kt b/core/src/main/kotlin/com/toasttab/expediter/TypeParsers.kt index bc5585c..8135ecf 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/TypeParsers.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/TypeParsers.kt @@ -22,6 +22,7 @@ import com.toasttab.expediter.types.MemberDescriptor import com.toasttab.expediter.types.MemberSymbolicReference import com.toasttab.expediter.types.MethodAccessType import com.toasttab.expediter.types.TypeDescriptor +import com.toasttab.expediter.types.TypeExtensibility import com.toasttab.expediter.types.TypeFlavor import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassReader.SKIP_DEBUG @@ -109,8 +110,9 @@ private class TypeDescriptorParser : ClassVisitor(ASM9) { private val members: MutableList> = mutableListOf() private var access: Int = 0 private var typeFlavor: TypeFlavor = TypeFlavor.UNKNOWN + private var typeExtensibility: TypeExtensibility = TypeExtensibility.UNKNOWN - fun get() = TypeDescriptor(name!!, superName, interfaces, members, AttributeParser.protection(access), typeFlavor) + fun get() = TypeDescriptor(name!!, superName, interfaces, members, AttributeParser.protection(access), typeFlavor, typeExtensibility) override fun visit( version: Int, @@ -123,7 +125,8 @@ private class TypeDescriptorParser : ClassVisitor(ASM9) { this.name = name this.superName = superName this.access = access - this.typeFlavor = if (AttributeParser.isInterface(access)) TypeFlavor.INTERFACE else TypeFlavor.CLASS + this.typeFlavor = AttributeParser.flavor(access) + this.typeExtensibility = AttributeParser.extensibility(access) this.interfaces.addAll(interfaces) } diff --git a/core/src/main/kotlin/com/toasttab/expediter/ignore/Ignore.kt b/core/src/main/kotlin/com/toasttab/expediter/ignore/Ignore.kt index acd8bdb..d6f5a77 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/ignore/Ignore.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/ignore/Ignore.kt @@ -19,41 +19,41 @@ import com.toasttab.expediter.types.MemberSymbolicReference import java.io.Serializable interface Ignore : Serializable { - fun ignore(caller: String?, type: String, ref: MemberSymbolicReference<*>?): Boolean + fun ignore(caller: String?, type: String?, ref: MemberSymbolicReference<*>?): Boolean companion object { val NOTHING: Ignore = object : Ignore { - override fun ignore(caller: String?, type: String, ref: MemberSymbolicReference<*>?) = false + override fun ignore(caller: String?, type: String?, ref: MemberSymbolicReference<*>?) = false } } class Not( private val ignore: Ignore ) : Ignore { - override fun ignore(caller: String?, type: String, ref: MemberSymbolicReference<*>?) = !ignore.ignore(caller, type, ref) + override fun ignore(caller: String?, type: String?, ref: MemberSymbolicReference<*>?) = !ignore.ignore(caller, type, ref) } class And( private vararg val ignores: Ignore ) : Ignore { - override fun ignore(caller: String?, type: String, ref: MemberSymbolicReference<*>?) = ignores.all { it.ignore(caller, type, ref) } + override fun ignore(caller: String?, type: String?, ref: MemberSymbolicReference<*>?) = ignores.all { it.ignore(caller, type, ref) } } class Or( private vararg val ignores: Ignore ) : Ignore { - override fun ignore(caller: String?, type: String, ref: MemberSymbolicReference<*>?) = ignores.any { it.ignore(caller, type, ref) } + override fun ignore(caller: String?, type: String?, ref: MemberSymbolicReference<*>?) = ignores.any { it.ignore(caller, type, ref) } } object IsConstructor : Ignore { - override fun ignore(caller: String?, type: String, ref: MemberSymbolicReference<*>?) = ref is MemberSymbolicReference.MethodSymbolicReference && ref.isConstructor() + override fun ignore(caller: String?, type: String?, ref: MemberSymbolicReference<*>?) = ref is MemberSymbolicReference.MethodSymbolicReference && ref.isConstructor() } object Caller { class StartsWith( private vararg val partial: String ) : Ignore { - override fun ignore(caller: String?, type: String, ref: MemberSymbolicReference<*>?) = caller != null && partial.any { caller.startsWith(it) } + override fun ignore(caller: String?, type: String?, ref: MemberSymbolicReference<*>?) = caller != null && partial.any { caller.startsWith(it) } } } @@ -61,14 +61,14 @@ interface Ignore : Serializable { class StartsWith( private vararg val partial: String ) : Ignore { - override fun ignore(caller: String?, type: String, ref: MemberSymbolicReference<*>?) = partial.any { type.startsWith(it) } + override fun ignore(caller: String?, type: String?, ref: MemberSymbolicReference<*>?) = partial.any { type != null && type.startsWith(it) } } } class Signature( private val signature: String ) : Ignore { - override fun ignore(caller: String?, type: String, ref: MemberSymbolicReference<*>?) = ref?.signature == signature + override fun ignore(caller: String?, type: String?, ref: MemberSymbolicReference<*>?) = ref?.signature == signature companion object { val IS_BLANK = Signature("()V") diff --git a/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt b/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt index c873220..5afd876 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt @@ -19,12 +19,6 @@ import com.toasttab.expediter.issue.Issue import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap -sealed class TypeHierarchy { - object NoType : TypeHierarchy() - class IncompleteTypeHierarchy(val type: TypeDescriptor, val missingType: Set) : TypeHierarchy() - class CompleteTypeHierarchy(val type: TypeDescriptor, val classes: Sequence) : TypeHierarchy() -} - private class ApplicationTypeContainer( val appTypes: Map, val duplicates: List @@ -64,7 +58,7 @@ class InspectedTypes private constructor( it.value.type } - private val hierarchyCache: MutableMap = hashMapOf() + private val hierarchyCache: MutableMap = hashMapOf() constructor(all: List, platformTypeProvider: PlatformTypeProvider) : this(ApplicationTypeContainer.create(all), platformTypeProvider) @@ -72,18 +66,18 @@ class InspectedTypes private constructor( return inspectedCache[s] ?: inspectedCache.computeIfAbsent(s) { s -> if (s.startsWith("[")) { // TODO: make up a type descriptor for an array type; we could validate that the element type actually exists - TypeDescriptor(s, "java/lang/Object", emptyList(), emptyList(), AccessProtection.UNKNOWN, TypeFlavor.CLASS) + TypeDescriptor(s, "java/lang/Object", emptyList(), emptyList(), AccessProtection.UNKNOWN, TypeFlavor.CLASS, TypeExtensibility.FINAL) } else { platformTypeProvider.lookupPlatformType(s) } } } - private fun traverse(cls: TypeDescriptor): TypeWithHierarchy { - val loaded = hierarchyCache[cls] + private fun traverse(cls: TypeDescriptor): TypeHierarchy { + val cached = hierarchyCache[cls] - if (loaded == null) { - val superTypes = mutableSetOf() + if (cached == null) { + val superTypes = mutableSetOf() val superTypeNames = mutableListOf() superTypeNames.addAll(cls.interfaces) @@ -93,76 +87,28 @@ class InspectedTypes private constructor( val l = lookup(s) if (l == null) { - superTypes.add(MaybeType.MissingType(s)) + superTypes.add(OptionalType.MissingType(s)) } else { - val loaded = traverse(l) - superTypes.add(MaybeType.Type(l)) - superTypes.addAll(loaded.superTypes) + val hierarchy = traverse(l) + superTypes.add(OptionalType.Type(l)) + superTypes.addAll(hierarchy.superTypes) } } - return TypeWithHierarchy(cls, superTypes).also { + return TypeHierarchy(cls, superTypes).also { hierarchyCache[cls] = it } } else { - return loaded + return cached } } - fun hierarchy(access: MemberAccess<*>) = when (access) { - is MemberAccess.FieldAccess -> hierarchy(access) - is MemberAccess.MethodAccess -> hierarchy(access) - } - - private fun hierarchy(access: MemberAccess.FieldAccess): TypeHierarchy { - val cls = lookup(access.targetType)?.let { traverse(it) } - - return if (cls == null) { - TypeHierarchy.NoType - } else { - val missing = cls.superTypes.filterIsInstance() - if (missing.isNotEmpty()) { - TypeHierarchy.IncompleteTypeHierarchy(cls.cls, missing.toSet()) - } else { - TypeHierarchy.CompleteTypeHierarchy(cls.cls, sequenceOf(cls.cls) + cls.superTypes.asSequence().filterIsInstance().map { it.cls }) - } - } + fun resolveHierarchy(type: String): OptionalResolvedTypeHierarchy { + return lookup(type)?.let { resolveHierarchy(it) } ?: OptionalResolvedTypeHierarchy.NoType } - private fun hierarchy(access: MemberAccess.MethodAccess): TypeHierarchy { - val cls = lookup(access.targetType)?.let { traverse(it) } - - return if (cls == null) { - TypeHierarchy.NoType - } else { - val missing = cls.superTypes.filterIsInstance() - if (missing.isNotEmpty()) { - TypeHierarchy.IncompleteTypeHierarchy(cls.cls, missing.toSet()) - } else if (access.accessType == MethodAccessType.VIRTUAL || - access.accessType == MethodAccessType.STATIC || - access.accessType == MethodAccessType.SPECIAL && !access.ref.isConstructor() - ) { - // invokevirtual / static / special (except for constructors) may refer to - // a method declared on target type or target type's supertypes - TypeHierarchy.CompleteTypeHierarchy( - cls.cls, - sequenceOf(cls.cls) + cls.superTypes.asSequence().filterIsInstance().map { it.cls } - ) - } else if (access.accessType == MethodAccessType.INTERFACE) { - // same story for invokeinterface, but method must be present on an interface type - TypeHierarchy.CompleteTypeHierarchy( - cls.cls, - sequenceOf(cls.cls) + cls.superTypes.asSequence() - .filterIsInstance() - .map { it.cls } - .filter { it.flavor != TypeFlavor.CLASS } - ) - } else { - // constructor must be present on the target type - // TODO: this will catch non-constructor cases, like indy, need to handle or ignore them properly - TypeHierarchy.CompleteTypeHierarchy(cls.cls, sequenceOf(cls.cls)) - } - } + fun resolveHierarchy(type: TypeDescriptor): ResolvedTypeHierarchy { + return traverse(type).resolve() } val classes: Collection get() = appTypes.appTypes.values diff --git a/core/src/main/kotlin/com/toasttab/expediter/types/TypeHierarchy.kt b/core/src/main/kotlin/com/toasttab/expediter/types/TypeHierarchy.kt new file mode 100644 index 0000000..d69d3d5 --- /dev/null +++ b/core/src/main/kotlin/com/toasttab/expediter/types/TypeHierarchy.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023 Toast Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.toasttab.expediter.types +class TypeHierarchy( + val type: TypeDescriptor, + val superTypes: Set +) { + fun resolve(): ResolvedTypeHierarchy { + val missing = superTypes.filterIsInstance() + return if (missing.isNotEmpty()) { + ResolvedTypeHierarchy.IncompleteTypeHierarchy(type, missing.toSet()) + } else { + ResolvedTypeHierarchy.CompleteTypeHierarchy( + type, + superTypes.asSequence().filterIsInstance().map { it.cls } + ) + } + } +} + +/** + * Sum type of [TypeDescriptor | MissingType = type not found on app's classpath] + */ +sealed class OptionalType { + abstract val name: String + + class Type(val cls: TypeDescriptor) : OptionalType() { + override val name: String + get() = cls.name + } + + class MissingType(override val name: String) : OptionalType() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OptionalType + + return name == other.name + } + + override fun hashCode() = name.hashCode() +} + +sealed interface OptionalResolvedTypeHierarchy { + object NoType : OptionalResolvedTypeHierarchy +} + +sealed interface ResolvedTypeHierarchy : OptionalResolvedTypeHierarchy { + class IncompleteTypeHierarchy(val type: TypeDescriptor, val missingType: Set) : ResolvedTypeHierarchy + class CompleteTypeHierarchy(val type: TypeDescriptor, val superTypes: Sequence) : ResolvedTypeHierarchy { + val allTypes: Sequence get() = sequenceOf(type) + superTypes + } +} + +class ApplicationTypeWithResolvedHierarchy( + val appType: ApplicationType, + val hierarchy: ResolvedTypeHierarchy +) : IdentifiesType by appType diff --git a/model/src/main/kotlin/com/toasttab/expediter/issue/Issue.kt b/model/src/main/kotlin/com/toasttab/expediter/issue/Issue.kt index fa9e336..cc3b18a 100644 --- a/model/src/main/kotlin/com/toasttab/expediter/issue/Issue.kt +++ b/model/src/main/kotlin/com/toasttab/expediter/issue/Issue.kt @@ -21,36 +21,55 @@ import kotlinx.serialization.Serializable @Serializable sealed interface Issue { - val target: String + val target: String? + val caller: String? @Serializable @SerialName("duplicate-type") data class DuplicateType(override val target: String, val sources: List) : Issue { override fun toString() = "duplicate class $target in $sources" + + override val caller = null } @Serializable @SerialName("type-missing") - data class MissingType(val caller: String, override val target: String) : Issue { + data class MissingType(override val caller: String, override val target: String) : Issue { override fun toString() = "$caller refers to missing type $target" } + @Serializable + @SerialName("application-supertype-missing") + data class MissingApplicationSuperType(override val caller: String, val missing: Set) : Issue { + override fun toString() = "$caller extends missing ${missing.readable}" + + override val target = null + } + + @Serializable + @SerialName("application-supertype-final") + data class FinalApplicationSuperType(override val caller: String, val final: Set) : Issue { + override fun toString() = "$caller extends final ${final.readable}" + + override val target = null + } + @Serializable @SerialName("supertype-missing") - data class MissingSuperType(val caller: String, override val target: String, val missing: Set) : Issue { - override fun toString() = "$caller refers to type $target with missing supertype $missing" + data class MissingSuperType(override val caller: String, override val target: String, val missing: Set) : Issue { + override fun toString() = "$caller refers to type $target with missing super${missing.readable}" } @Serializable @SerialName("method-missing") - data class MissingMember(val caller: String, val member: MemberAccess<*>) : Issue { + data class MissingMember(override val caller: String, val member: MemberAccess<*>) : Issue { override val target: String get() = member.targetType override fun toString() = "$caller accesses missing $member" } @Serializable @SerialName("static-member") - data class AccessStaticMemberNonStatically(val caller: String, val member: MemberAccess<*>) : Issue { + data class AccessStaticMemberNonStatically(override val caller: String, val member: MemberAccess<*>) : Issue { override val target: String get() = member.targetType override fun toString() = "$caller accesses static $member non-statically" @@ -58,15 +77,21 @@ sealed interface Issue { @Serializable @SerialName("instance-member") - data class AccessInstanceMemberStatically(val caller: String, val member: MemberAccess<*>) : Issue { + data class AccessInstanceMemberStatically(override val caller: String, val member: MemberAccess<*>) : Issue { override val target: String get() = member.targetType override fun toString() = "$caller accesses instance $member statically" } @Serializable @SerialName("member-inaccessible") - data class AccessInaccessibleMember(val caller: String, val member: MemberAccess<*>) : Issue { + data class AccessInaccessibleMember(override val caller: String, val member: MemberAccess<*>) : Issue { override val target: String get() = member.targetType override fun toString() = "$caller accesses inaccessible $member" } } + +private val Collection.readable: String get() = if (size == 1) { + "type " + first() +} else { + "types " + joinToString(", ") +} diff --git a/model/src/main/kotlin/com/toasttab/expediter/types/TypeDescriptor.kt b/model/src/main/kotlin/com/toasttab/expediter/types/TypeDescriptor.kt index 7fb35e0..9b2ab33 100644 --- a/model/src/main/kotlin/com/toasttab/expediter/types/TypeDescriptor.kt +++ b/model/src/main/kotlin/com/toasttab/expediter/types/TypeDescriptor.kt @@ -25,51 +25,27 @@ enum class TypeFlavor { CLASS, INTERFACE, UNKNOWN } +enum class TypeExtensibility { + FINAL, NOT_FINAL, UNKNOWN +} + +interface IdentifiesType { + val name: String +} + /** * Represents declared properties (fields / methods / directly extended supertypes) of a type. */ class TypeDescriptor( - val name: String, + override val name: String, val superName: String?, val interfaces: List, val members: List>, val protection: AccessProtection, - val flavor: TypeFlavor -) - -/** - * Sum type of [TypeDescriptor | MissingType = type not found on app's classpath] - */ -sealed class MaybeType { - abstract val name: String - - class Type(val cls: TypeDescriptor) : MaybeType() { - override val name: String - get() = cls.name - } - - class MissingType(override val name: String) : MaybeType() - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as MaybeType - - return name == other.name - } - - override fun hashCode() = name.hashCode() -} - -/** - * Represents declared properties and all supertypes of a type. - */ -class TypeWithHierarchy( - val cls: TypeDescriptor, - val superTypes: Set -) + val flavor: TypeFlavor, + val extensibility: TypeExtensibility +) : IdentifiesType /** * Represents declared properties of a type and all fields / methods that type's code accesses / invokes. @@ -78,6 +54,6 @@ class ApplicationType( val type: TypeDescriptor, val refs: Set>, val source: String -) { +) : IdentifiesType by type { override fun toString() = "ApplicationType[${type.name}]" } diff --git a/tests/lib1/src/main/java/com/toasttab/expediter/test/Bar.java b/tests/lib1/src/main/java/com/toasttab/expediter/test/Bar.java index 0606d33..b5e9885 100644 --- a/tests/lib1/src/main/java/com/toasttab/expediter/test/Bar.java +++ b/tests/lib1/src/main/java/com/toasttab/expediter/test/Bar.java @@ -26,4 +26,6 @@ public void bar(String x) { } public void bar(int x) { } public void bar(long x) { } + + public void bar(float x) { } } diff --git a/tests/lib1/src/main/java/com/toasttab/expediter/test/Base.java b/tests/lib1/src/main/java/com/toasttab/expediter/test/Base.java new file mode 100644 index 0000000..884a38d --- /dev/null +++ b/tests/lib1/src/main/java/com/toasttab/expediter/test/Base.java @@ -0,0 +1,5 @@ +package com.toasttab.expediter.test; + +public class Base { + public int w; +} diff --git a/tests/lib2/src/main/java/com/toasttab/expediter/test/Bar.java b/tests/lib2/src/main/java/com/toasttab/expediter/test/Bar.java index b691674..14deaf7 100644 --- a/tests/lib2/src/main/java/com/toasttab/expediter/test/Bar.java +++ b/tests/lib2/src/main/java/com/toasttab/expediter/test/Bar.java @@ -29,4 +29,7 @@ private void bar(int x) { } // changes from public to package-private void bar(long x) { } + + // changes from public to protected + protected void bar(float x) { } } diff --git a/tests/lib2/src/main/java/com/toasttab/expediter/test/Base.java b/tests/lib2/src/main/java/com/toasttab/expediter/test/Base.java new file mode 100644 index 0000000..2881e34 --- /dev/null +++ b/tests/lib2/src/main/java/com/toasttab/expediter/test/Base.java @@ -0,0 +1,6 @@ +package com.toasttab.expediter.test; + +// changes to final +public final class Base { + protected int w; +} diff --git a/tests/lib2/src/main/java/com/toasttab/expediter/test/BaseBar.java b/tests/lib2/src/main/java/com/toasttab/expediter/test/BaseBar.java index c0fe684..5e176e1 100644 --- a/tests/lib2/src/main/java/com/toasttab/expediter/test/BaseBar.java +++ b/tests/lib2/src/main/java/com/toasttab/expediter/test/BaseBar.java @@ -1,6 +1,7 @@ package com.toasttab.expediter.test; class BaseBar { + // public field accessed via public subclass (even though this class is package-private), this is fine public int i; // changes from public to package-private diff --git a/tests/src/main/java/com/toasttab/expediter/test/caller/Caller.java b/tests/src/main/java/com/toasttab/expediter/test/caller/Caller.java index 96a306e..5bc95c4 100644 --- a/tests/src/main/java/com/toasttab/expediter/test/caller/Caller.java +++ b/tests/src/main/java/com/toasttab/expediter/test/caller/Caller.java @@ -16,11 +16,12 @@ package com.toasttab.expediter.test.caller; import com.toasttab.expediter.test.Bar; +import com.toasttab.expediter.test.Base; import com.toasttab.expediter.test.BaseFoo; import com.toasttab.expediter.test.Baz; import com.toasttab.expediter.test.Foo; -public class Caller { +public final class Caller extends Base { Foo foo; BaseFoo baseFoo; Bar bar; @@ -69,9 +70,15 @@ void packagePrivateField() { bar.j = 1; } + void protectedField() { + new Baz().bar(1f); + } + void fieldMovedFromSuper() { new Baz().i = 1; } void fieldAccessedViaPublicSubclass() { bar.i = 1; } + + void accessProtectedField() { w = 0; } } diff --git a/tests/src/test/kotlin/com/toasttab/expediter/test/ExpediterIntegrationTest.kt b/tests/src/test/kotlin/com/toasttab/expediter/test/ExpediterIntegrationTest.kt index 43c97ef..de4f822 100644 --- a/tests/src/test/kotlin/com/toasttab/expediter/test/ExpediterIntegrationTest.kt +++ b/tests/src/test/kotlin/com/toasttab/expediter/test/ExpediterIntegrationTest.kt @@ -81,6 +81,16 @@ class ExpediterIntegrationTest { ) ), + Issue.AccessInaccessibleMember( + "com/toasttab/expediter/test/caller/Caller", + MemberAccess.MethodAccess( + "com/toasttab/expediter/test/Baz", + "com/toasttab/expediter/test/Bar", + MethodSymbolicReference("bar", "(F)V"), + MethodAccessType.VIRTUAL + ) + ), + Issue.MissingMember( "com/toasttab/expediter/test/caller/Caller", MemberAccess.FieldAccess( @@ -142,9 +152,19 @@ class ExpediterIntegrationTest { "com/toasttab/expediter/test/BaseFoo" ), + Issue.MissingApplicationSuperType( + "com/toasttab/expediter/test/Foo", + setOf("com/toasttab/expediter/test/BaseFoo") + ), + Issue.DuplicateType( "com/toasttab/expediter/test/Dupe", listOf("main", "lib2.jar") + ), + + Issue.FinalApplicationSuperType( + "com/toasttab/expediter/test/caller/Caller", + setOf("com/toasttab/expediter/test/Base") ) ) }