Skip to content

fix: detect usage of more kinds of import, e.g. of inner classes. #1505

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package com.autonomousapps.jvm

import com.autonomousapps.jvm.projects.ConstantsProject
import com.autonomousapps.utils.Colors

import static com.autonomousapps.utils.Runner.build
import static com.google.common.truth.Truth.assertThat
Expand All @@ -24,9 +25,9 @@ final class ConstantsSpec extends AbstractJvmSpec {
gradleVersion << gradleVersions()
}

def "detects top-level constants from Kotlin source (#gradleVersion)"() {
def "detects top-level constants from Java source (#gradleVersion)"() {
given:
def project = new ConstantsProject.TopLevel()
def project = new ConstantsProject.Java()
gradleProject = project.gradleProject

when:
Expand All @@ -39,6 +40,33 @@ final class ConstantsSpec extends AbstractJvmSpec {
gradleVersion << gradleVersions()
}

def "detects nested constants from Java source (#gradleVersion)"() {
given:
def project = new ConstantsProject.JavaNested()
gradleProject = project.gradleProject

when:
def result = build(
gradleVersion, gradleProject.rootDir,
'buildHealth',
':consumer:reason', '--id', ':producer',
)

then:
assertThat(project.actualBuildHealth()).containsExactlyElementsIn(project.expectedBuildHealth)

and:
assertThat(Colors.decolorize(result.output)).contains(
'''\
Source: main
------------
* Uses 4 constants: CONSTANT, DOUBLE_CONST, FLOAT_CONST, LONG_CONST (implies implementation).'''.stripIndent()
)

where:
gradleVersion << gradleVersions()
}

def "detects nested constants from Kotlin source (#gradleVersion)"() {
given:
def project = new ConstantsProject.Nested()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,90 @@ final class ConstantsProject {
]
}

final static class JavaNested extends AbstractProject {

final GradleProject gradleProject
private final libProject = project('implementation', ':producer')

JavaNested() {
this.gradleProject = build()
}

private GradleProject build() {
return newGradleProjectBuilder()
.withSubproject('consumer') { s ->
s.sources = SOURCES_CONSUMER
s.withBuildScript { bs ->
bs.plugins = kotlin
bs.dependencies(libProject)
}
}
.withSubproject('producer') { s ->
s.sources = SOURCES_PRODUCER
s.withBuildScript { bs ->
bs.plugins = javaLibrary
}
}
.write()
}

private static final List<Source> SOURCES_CONSUMER = [
Source.kotlin(
'''\
package com.example

import com.example.library.Library.Inner

public class Main {
fun useConstant() {
println(Inner.CONSTANT)
println(Inner.INT_CONST)
println(Inner.FLOAT_CONST)
println(Inner.LONG_CONST)
println(Inner.DOUBLE_CONST)
}
}'''.stripIndent()
)
.withPath('com.example', 'Main')
.build()
]

private static final List<Source> SOURCES_PRODUCER = [
Source.java(
'''\
package com.example.library;

public class Library {
public static class Inner {
public static final String CONSTANT = "magic";
public static final int INT_CONST = 9;
public static final float FLOAT_CONST = 4.2f;
public static final long LONG_CONST = 11;
public static final double DOUBLE_CONST = 3.14d;
// A constant reference to a reference type (including arrays) other than strings triggers different
// heuristics. Null values do too (even for strings).
//public static final String NULL_CONST = null;
//public static final Object NULL_CONST = null;
//public static final int[] DOUBLE_ARR_CONST = new int[0];
//public static final String[] STRING_ARR_CONST = new String[0];
//public static final Class<?> CLASS_CONST = String.class;
}
}'''.stripIndent()
)
.withPath('com.example.library', 'Library')
.build()
]

Set<ProjectAdvice> actualBuildHealth() {
return actualProjectAdvice(gradleProject)
}

final Set<ProjectAdvice> expectedBuildHealth = [
emptyProjectAdviceFor(':consumer'),
emptyProjectAdviceFor(':producer'),
]
}

final static class TopLevel extends AbstractProject {

final GradleProject gradleProject
Expand Down
42 changes: 11 additions & 31 deletions src/main/kotlin/com/autonomousapps/internal/BytecodeParsers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,9 @@ internal class ClassFilesParser(
override fun parseBytecode(): Set<ExplodingBytecode> {
return classes.asSequenceOfClassFiles()
.map { classFile ->
val classFilePath = classFile.path
val explodedClass = classFile.inputStream().use {
BytecodeReader(it.readBytes(), logger, classFilePath).parse()
classFile.inputStream().use {
BytecodeReader(it.readBytes(), logger, classFile.path, relativize(classFile)).parse()
}

ExplodingBytecode(
relativePath = relativize(classFile),
className = explodedClass.className,
superClass = explodedClass.superClass,
interfaces = explodedClass.interfaces,
sourceFile = explodedClass.source,
nonAnnotationClasses = explodedClass.nonAnnotationClasses,
nonAnnotationClassesWithinVisibleAnnotation = explodedClass.nonAnnotationClassesWithinVisibleAnnotation,
annotationClasses = explodedClass.annotationClasses,
binaryClassAccesses = explodedClass.binaryClasses,
)
}
.toSortedSet()
}
Expand All @@ -64,6 +51,7 @@ private class BytecodeReader(
private val bytes: ByteArray,
private val logger: Logger,
private val classFilePath: String,
private val relativePath: String,
) {
/**
* This (currently, maybe forever) fails to detect constant usage in Kotlin-generated class files.
Expand All @@ -74,7 +62,7 @@ private class BytecodeReader(
* 1. The "source" of the class file (the source file name, like "Main.kt").
* 2. The classes used by that class file.
*/
fun parse(): ExplodedClass {
fun parse(): ExplodingBytecode {
val constantPool = ConstantPoolParser.getConstantPoolClassReferences(bytes, classFilePath)
// Constant pool has a lot of weird bullshit in it
.filter { JAVA_FQCN_REGEX_SLASHY.matches(it) }
Expand Down Expand Up @@ -102,15 +90,18 @@ private class BytecodeReader(
.map { it.classRef }
.toSet()

return ExplodedClass(
source = classAnalyzer.source,
return ExplodingBytecode(
relativePath = relativePath,
className = canonicalize(classAnalyzer.className),
superClass = classAnalyzer.superClass?.let { canonicalize(it) },
interfaces = classAnalyzer.interfaces.asSequence().fixup(classAnalyzer),
sourceFile = classAnalyzer.source,
nonAnnotationClasses = constantPool.asSequence().plus(usedNonAnnotationClasses).fixup(classAnalyzer),
nonAnnotationClassesWithinVisibleAnnotation = usedNonAnnotationClassesWithinVisibleAnnotation.asSequence().fixup(classAnalyzer),
nonAnnotationClassesWithinVisibleAnnotation = usedNonAnnotationClassesWithinVisibleAnnotation.asSequence()
.fixup(classAnalyzer),
annotationClasses = usedAnnotationClasses.asSequence().fixup(classAnalyzer),
binaryClasses = classAnalyzer.getBinaryClasses().fixup(classAnalyzer),
inferredConstants = classAnalyzer.getInferredConstants(),
binaryClassAccesses = classAnalyzer.getBinaryClasses().fixup(classAnalyzer),
)
}

Expand Down Expand Up @@ -150,14 +141,3 @@ private class BytecodeReader(
.filterKeys { it != classAnalyzer.className }
}
}

private class ExplodedClass(
val source: String?,
val className: String,
val superClass: String?,
val interfaces: Set<String>,
val nonAnnotationClasses: Set<String>,
val nonAnnotationClassesWithinVisibleAnnotation: Map<String, String>,
val annotationClasses: Set<String>,
val binaryClasses: Map<String, Set<MemberAccess>>,
)
Loading