From 9d2554589a83e32ce51f6cf339cb726ba135db8b Mon Sep 17 00:00:00 2001 From: Jordi Gil Date: Mon, 9 Nov 2026 10:19:13 -0500 Subject: [PATCH 01/10] feat: preserve adapter context handoff metadata --- docs/flpath-5002-java-adapter.md | 7 ++- .../analysis/java/AnalysisContext.java | 55 ++++++++++++++++-- .../analysis/java/AnalysisContextLoader.java | 5 +- .../analysis/java/SemanticCallFacts.java | 58 +++++++++++++++++-- .../java/SemanticCallFactsJsonWriter.java | 15 +++++ .../java/AnalysisContextLoaderTest.java | 29 ++++++++-- 6 files changed, 148 insertions(+), 21 deletions(-) diff --git a/docs/flpath-5002-java-adapter.md b/docs/flpath-5002-java-adapter.md index 28fc587..07d7aa8 100644 --- a/docs/flpath-5002-java-adapter.md +++ b/docs/flpath-5002-java-adapter.md @@ -58,9 +58,10 @@ policy, controller, runtime validation, or durable repository-profile wiring. ## Manual VPE integration The VPE `AnalysisContext` JSON handoff is loaded by -`AnalysisContextLoader`. The manual command preserves source/build/provenance -fingerprints and emits normalized facts with separate provider and framework -sets: +`AnalysisContextLoader`. The manual command preserves the selected profile, +production/test scope, generated-root declarations, source/build/provenance +fingerprints, and handoff limitations. It emits normalized facts with separate +provider and framework sets: ```sh spike/scripts/run-manual-integration.sh diff --git a/src/main/java/com/projectlightpath/analysis/java/AnalysisContext.java b/src/main/java/com/projectlightpath/analysis/java/AnalysisContext.java index b135d09..ccb94c0 100644 --- a/src/main/java/com/projectlightpath/analysis/java/AnalysisContext.java +++ b/src/main/java/com/projectlightpath/analysis/java/AnalysisContext.java @@ -57,7 +57,19 @@ public record SourceRoot( Scope scope, boolean generated, boolean included, - boolean required) { + boolean required, + String fingerprint) { + /** Compatibility constructor for callers that do not provide a root fingerprint. */ + public SourceRoot( + String declaredPath, + Path path, + Scope scope, + boolean generated, + boolean included, + boolean required) { + this(declaredPath, path, scope, generated, included, required, null); + } + /** Validates and normalizes one source root declaration. */ public SourceRoot { if (declaredPath == null || declaredPath.isBlank()) { @@ -70,12 +82,27 @@ public record SourceRoot( /** Creates an included application source root. */ public static SourceRoot source(String declaredPath, Path path, Scope scope, boolean required) { - return new SourceRoot(declaredPath, path, scope, false, true, required); + return source(declaredPath, path, scope, required, null); + } + + public static SourceRoot source( + String declaredPath, Path path, Scope scope, boolean required, String fingerprint) { + return new SourceRoot(declaredPath, path, scope, false, true, required, fingerprint); } public static SourceRoot generated( String declaredPath, Path path, Scope scope, boolean included, boolean required) { - return new SourceRoot(declaredPath, path, scope, true, included, required); + return generated(declaredPath, path, scope, included, required, null); + } + + public static SourceRoot generated( + String declaredPath, + Path path, + Scope scope, + boolean included, + boolean required, + String fingerprint) { + return new SourceRoot(declaredPath, path, scope, true, included, required, fingerprint); } } @@ -616,14 +643,32 @@ public Builder analysisScope(Scope value) { /** Adds an included application source root. */ public Builder sourceRoot(String declaredPath, Path path, Scope scope, boolean required) { - sourceRoots.add(SourceRoot.source(declaredPath, path, scope, required)); + return sourceRoot(declaredPath, path, scope, required, null); + } + + /** Adds an included application source root with its handoff fingerprint. */ + public Builder sourceRoot( + String declaredPath, Path path, Scope scope, boolean required, String fingerprint) { + sourceRoots.add(SourceRoot.source(declaredPath, path, scope, required, fingerprint)); return this; } /** Adds an included or excluded generated source root. */ public Builder generatedSourceRoot( String declaredPath, Path path, Scope scope, boolean included, boolean required) { - generatedSourceRoots.add(SourceRoot.generated(declaredPath, path, scope, included, required)); + return generatedSourceRoot(declaredPath, path, scope, included, required, null); + } + + /** Adds an included or excluded generated source root with its handoff fingerprint. */ + public Builder generatedSourceRoot( + String declaredPath, + Path path, + Scope scope, + boolean included, + boolean required, + String fingerprint) { + generatedSourceRoots.add( + SourceRoot.generated(declaredPath, path, scope, included, required, fingerprint)); return this; } diff --git a/src/main/java/com/projectlightpath/analysis/java/AnalysisContextLoader.java b/src/main/java/com/projectlightpath/analysis/java/AnalysisContextLoader.java index 621f210..24c5971 100644 --- a/src/main/java/com/projectlightpath/analysis/java/AnalysisContextLoader.java +++ b/src/main/java/com/projectlightpath/analysis/java/AnalysisContextLoader.java @@ -195,12 +195,13 @@ private static void addRoot( AnalysisContext.Scope scope = parseScope(root.scope()); boolean generated = generatedCollection || root.generated(); if (generated) { - builder.generatedSourceRoot(declaredPath, resolved, scope, root.included(), root.required()); + builder.generatedSourceRoot( + declaredPath, resolved, scope, root.included(), root.required(), root.fingerprint()); } else { if (root.required() && !root.included()) { throw new IllegalArgumentException("required source root must be included: " + declaredPath); } - builder.sourceRoot(declaredPath, resolved, scope, root.required()); + builder.sourceRoot(declaredPath, resolved, scope, root.required(), root.fingerprint()); } } diff --git a/src/main/java/com/projectlightpath/analysis/java/SemanticCallFacts.java b/src/main/java/com/projectlightpath/analysis/java/SemanticCallFacts.java index a68886e..67bd508 100644 --- a/src/main/java/com/projectlightpath/analysis/java/SemanticCallFacts.java +++ b/src/main/java/com/projectlightpath/analysis/java/SemanticCallFacts.java @@ -3,10 +3,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; /** Provenance-preserving provider and framework relationships for one analysis context. */ public final class SemanticCallFacts { @@ -337,7 +339,45 @@ public record RunMetadata( int graphMethodCount, int graphCallCount, AnalysisContext.Provenance provenance, - AnalysisContext.Handoff handoff) { + AnalysisContext.Handoff handoff, + List generatedSourceRoots) { + public RunMetadata { + generatedSourceRoots = List.copyOf( + generatedSourceRoots == null ? List.of() : generatedSourceRoots); + } + + /** Compatibility constructor for callers that already provide build handoff metadata. */ + public RunMetadata( + String contextId, + String framework, + String frameworkVersion, + String fixtureId, + String selectedProfile, + String analysisScope, + int javaRelease, + String providerName, + String providerVersion, + int graphMethodCount, + int graphCallCount, + AnalysisContext.Provenance provenance, + AnalysisContext.Handoff handoff) { + this( + contextId, + framework, + frameworkVersion, + fixtureId, + selectedProfile, + analysisScope, + javaRelease, + providerName, + providerVersion, + graphMethodCount, + graphCallCount, + provenance, + handoff, + List.of()); + } + /** Compatibility constructor for callers that predate the JSON handoff metadata. */ public RunMetadata( String contextId, @@ -365,7 +405,8 @@ public RunMetadata( graphMethodCount, graphCallCount, provenance, - AnalysisContext.Handoff.empty()); + AnalysisContext.Handoff.empty(), + List.of()); } } @@ -410,7 +451,8 @@ public SemanticCallFacts( graph.methodCount(), graph.callCount(), context.provenance(), - context.handoff()); + context.handoff(), + context.generatedSourceRoots()); validateLinks(); } @@ -457,8 +499,14 @@ public EnumSet certaintyStates() { /** Validates fact-set ownership and freezes facts for the result. */ private static List immutableFacts(List facts, FactSet expectedSet) { List result = List.copyOf(facts == null ? List.of() : facts); - if (result.stream().anyMatch(fact -> fact.factSet() != expectedSet)) { - throw new IllegalArgumentException("facts must remain in their declared relationship set"); + Set ids = new HashSet<>(); + for (Fact fact : result) { + if (fact.factSet() != expectedSet) { + throw new IllegalArgumentException("facts must remain in their declared relationship set"); + } + if (!ids.add(fact.id())) { + throw new IllegalArgumentException("fact IDs must be unique within a relationship set: " + fact.id()); + } } return result; } diff --git a/src/main/java/com/projectlightpath/analysis/java/SemanticCallFactsJsonWriter.java b/src/main/java/com/projectlightpath/analysis/java/SemanticCallFactsJsonWriter.java index ac4efb5..16d1a45 100644 --- a/src/main/java/com/projectlightpath/analysis/java/SemanticCallFactsJsonWriter.java +++ b/src/main/java/com/projectlightpath/analysis/java/SemanticCallFactsJsonWriter.java @@ -64,6 +64,21 @@ private static Map runMetadata(SemanticCallFacts.RunMetadata met result.put("graphCallCount", metadata.graphCallCount()); result.put("provenance", provenance(metadata.provenance())); result.put("handoff", handoff(metadata.handoff())); + result.put("generatedSourceRoots", metadata.generatedSourceRoots().stream() + .map(SemanticCallFactsJsonWriter::sourceRoot) + .toList()); + return result; + } + + private static Map sourceRoot(AnalysisContext.SourceRoot root) { + Map result = new LinkedHashMap<>(); + result.put("declaredPath", root.declaredPath()); + result.put("path", root.path().toString()); + result.put("scope", root.scope().id()); + result.put("generated", root.generated()); + result.put("included", root.included()); + result.put("required", root.required()); + result.put("fingerprint", root.fingerprint()); return result; } diff --git a/src/test/java/com/projectlightpath/analysis/java/AnalysisContextLoaderTest.java b/src/test/java/com/projectlightpath/analysis/java/AnalysisContextLoaderTest.java index 367d34a..566713b 100644 --- a/src/test/java/com/projectlightpath/analysis/java/AnalysisContextLoaderTest.java +++ b/src/test/java/com/projectlightpath/analysis/java/AnalysisContextLoaderTest.java @@ -17,9 +17,11 @@ class AnalysisContextLoaderTest { @Test void loadsVpeContextAndPreservesBuildAndProvenanceHandoff() throws IOException { Path sourceRoot = temp.resolve("src/main/java"); + Path generatedRoot = temp.resolve("target/generated-sources"); Path classes = temp.resolve("target/classes"); Path dependency = temp.resolve("dependency.jar"); Files.createDirectories(sourceRoot); + Files.createDirectories(generatedRoot); Files.createDirectories(classes); Files.writeString(sourceRoot.resolve("Application.java"), "package demo; class Application {}\n"); Files.writeString(dependency, "dependency"); @@ -48,7 +50,15 @@ void loadsVpeContextAndPreservesBuildAndProvenanceHandoff() throws IOException { "required": true, "fingerprint": "source-fingerprint" }], - "generatedSourceRoots": [], + "generatedSourceRoots": [{ + "declaredPath": "target/generated-sources", + "path": "%s", + "scope": "production", + "generated": true, + "included": true, + "required": false, + "fingerprint": "generated-root-fingerprint" + }], "dependencyClasspath": ["%s"], "applicationClasses": "%s", "frameworkMetadata": [{ @@ -115,11 +125,12 @@ void loadsVpeContextAndPreservesBuildAndProvenanceHandoff() throws IOException { "limitations": ["manual run"] } """.formatted( - temp, - temp, - sourceRoot, - dependency, - classes)); + temp, + temp, + sourceRoot, + generatedRoot, + dependency, + classes)); AnalysisContext context = AnalysisContextLoader.load(contextFile); @@ -129,6 +140,7 @@ void loadsVpeContextAndPreservesBuildAndProvenanceHandoff() throws IOException { assertTrue(context.dependencyClasspathRequired()); assertEquals("0123456789012345678901234567890123456789", context.provenance().gitRevision()); assertEquals("dependency-fingerprint", context.provenance().dependencyFingerprint()); + assertEquals("generated-root-fingerprint", context.generatedSourceRoots().get(0).fingerprint()); assertEquals("maven", context.handoff().buildTool()); assertEquals(List.of("mvn", "package"), context.handoff().buildCommand()); assertEquals(List.of("manual run"), context.handoff().limitations()); @@ -139,5 +151,10 @@ void loadsVpeContextAndPreservesBuildAndProvenanceHandoff() throws IOException { assertEquals("demo-service", context.resolvedBindings().get(0).candidateId()); assertEquals(1, context.proxyTargetMetadata().size()); assertEquals("call", context.proxyTargetMetadata().get(0).methodName()); + + Path factsFile = temp.resolve("semantic-call-facts.json"); + SemanticCallFactsJsonWriter.write( + new JavaAnalysisAdapter().analyze(context, AnalysisGraph.empty()), factsFile); + assertTrue(Files.readString(factsFile).contains("\"fingerprint\" : \"generated-root-fingerprint\"")); } } From b429c24c36cfc3356eccf37573ef5768d97a210d Mon Sep 17 00:00:00 2001 From: Jordi Gil Date: Mon, 9 Nov 2026 10:19:23 -0500 Subject: [PATCH 02/10] feat: harden SootUp and framework enrichment --- .../analysis/java/BoundedFrameworkModel.java | 177 +- .../analysis/java/DynamicFeature.java | 32 +- .../analysis/java/JavaAnalysisAdapter.java | 5 +- .../analysis/java/MethodSymbol.java | 102 +- .../analysis/java/ProviderFactNormalizer.java | 102 +- .../java/SootUpBaseGraphProvider.java | 37 +- .../analysis/java/SourceMetadataScanner.java | 3402 ++++++++++++++++- 7 files changed, 3738 insertions(+), 119 deletions(-) diff --git a/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java b/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java index b7cb3ac..51e04ae 100644 --- a/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java +++ b/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java @@ -561,7 +561,7 @@ private List generatedFacts( : SemanticCallFacts.Certainty.UNRESOLVED; String callee = generated.file().typeName() + "#" + generated.method().name(); facts.add(SemanticCallFacts.Fact.framework( - generatedFactId(generated), + generatedFactId(scanner, generated), context, "quarkus:generated-source:" + generated.file().typeName(), callee, @@ -585,6 +585,7 @@ private List generatedFacts( private List dynamicProxyFacts( AnalysisContext context, SourceMetadataScanner scanner) { List facts = new ArrayList<>(); + Set factIds = new LinkedHashSet<>(); for (DynamicFeature feature : scanner.dynamicFeatures()) { if (feature.kind() != DynamicFeature.Kind.DYNAMIC_PROXY) { continue; @@ -593,8 +594,12 @@ private List dynamicProxyFacts( ? feature.target().substring(0, feature.target().indexOf('#')) : feature.target(); String caller = framework.id() + ":dynamic-proxy:" + targetOwner; + String factId = uniqueFactId( + factIds, + dynamicProxyFactId(scanner, feature), + dynamicFeatureIdentity(feature)); facts.add(SemanticCallFacts.Fact.framework( - dynamicProxyFactId(feature), + factId, context, caller, feature.target(), @@ -605,8 +610,8 @@ private List dynamicProxyFacts( "dynamic_proxy_call", feature.sourcePath(), feature.symbols(), - null, - Map.of("caller", feature.caller().displayName()))), + feature.sourceLine() > 0 ? feature.sourceLine() : null, + dynamicProxyEvidenceAttributes(feature))), feature.limitation())); } return facts; @@ -617,17 +622,15 @@ private List springRepositoryFacts( AnalysisContext context, SourceMetadataScanner scanner) { List facts = new ArrayList<>(); boolean metadataAvailable = context.metadataObserved("spring-data-repository-contract"); - for (SourceMetadataScanner.BeanCandidate candidate : scanner.beanCandidates(framework)) { + List candidates = scanner.beanCandidates(framework); + for (SourceMetadataScanner.BeanCandidate candidate : candidates) { if (!candidate.kind().equals("repository_interface")) { continue; } SemanticCallFacts.Certainty certainty = metadataAvailable ? SemanticCallFacts.Certainty.POSSIBLE : SemanticCallFacts.Certainty.UNRESOLVED; - String simple = candidate.file().simpleTypeName(); - String id = simple.equals("AccountRepository") - ? "spring-data-generated-account-repository" - : "spring-data-generated-" + slug(simple); + String id = springRepositoryFactId(scanner, candidates, candidate); facts.add(SemanticCallFacts.Fact.framework( id, context, @@ -662,7 +665,7 @@ private List springLifecycleFacts( continue; } facts.add(SemanticCallFacts.Fact.framework( - lifecycleFactId(file, method, "bean"), + lifecycleFactId(scanner, file, method, "bean"), context, "Spring ApplicationContext", file.typeName() + "#" + method.name(), @@ -684,7 +687,7 @@ private List springLifecycleFacts( continue; } facts.add(SemanticCallFacts.Fact.framework( - lifecycleFactId(file, method, "runner"), + lifecycleFactId(scanner, file, method, "runner"), context, "Spring Boot application lifecycle", file.typeName() + "#run", @@ -1617,29 +1620,161 @@ private static boolean uniqueMethodName( .count() == 1; } - private String generatedFactId(SourceMetadataScanner.GeneratedMethod generated) { + private String generatedFactId( + SourceMetadataScanner scanner, + SourceMetadataScanner.GeneratedMethod generated) { if (generated.file().simpleTypeName().equals("GeneratedTransferMapper") - && generated.method().name().equals("toResponse")) { + && generated.method().name().equals("toResponse") + && uniqueSimpleType(scanner, generated.file().simpleTypeName()) + && uniqueMethodName(generated.file(), generated.method().name()) + && scanner.generatedMethods().stream() + .filter(item -> item.file().simpleTypeName().equals(generated.file().simpleTypeName())) + .filter(item -> item.method().name().equals(generated.method().name())) + .count() == 1) { return "quarkus-generated-mapper-binding"; } - return "quarkus-generated-" + slug(generated.file().simpleTypeName() + "-" + generated.method().name()); + String identity = String.join("\u0000", + framework.id(), + generated.file().typeName(), + generated.file().displayPath(), + generated.method().name(), + generated.method().parameters().replaceAll("\\s+", " ").trim(), + generated.method().returnType(), + Integer.toString(generated.method().start())); + return framework.id() + "-generated-" + + slug(generated.file().typeName() + "-" + generated.method().name()) + + "-" + digest(identity).substring(0, 16); } - private String dynamicProxyFactId(DynamicFeature feature) { - if (framework == AnalysisContext.Framework.QUARKUS && feature.target().contains("AuthCallback#onAuthenticated")) { + private String dynamicProxyFactId(SourceMetadataScanner scanner, DynamicFeature feature) { + if (framework == AnalysisContext.Framework.QUARKUS + && dynamicTargetSimpleName(feature.target()).equals("AuthCallback") + && dynamicTargetMethod(feature.target()).equals("onAuthenticated") + && scanner.dynamicFeatures().stream() + .filter(item -> item.kind() == DynamicFeature.Kind.DYNAMIC_PROXY) + .filter(item -> dynamicTargetSimpleName(item.target()).equals("AuthCallback")) + .filter(item -> dynamicTargetMethod(item.target()).equals("onAuthenticated")) + .count() == 1) { return "quarkus-unbounded-proxy-abstention"; } - return framework.id() + "-dynamic-proxy-" + slug(feature.target()); + return framework.id() + "-dynamic-proxy-" + slug(feature.target()) + + "-" + digest(dynamicFeatureIdentity(feature)).substring(0, 16); + } + + private String springRepositoryFactId( + SourceMetadataScanner scanner, + List candidates, + SourceMetadataScanner.BeanCandidate candidate) { + boolean uniqueLegacyRepository = candidate.file().simpleTypeName().equals("AccountRepository") + && uniqueSimpleType(scanner, candidate.file().simpleTypeName()) + && candidates.stream() + .filter(item -> item.kind().equals("repository_interface")) + .filter(item -> item.file().simpleTypeName().equals("AccountRepository")) + .count() == 1 + && SourceMetadataScanner.methods(candidate.file()).stream() + .filter(method -> method.name().equals("findById")) + .count() <= 1; + if (uniqueLegacyRepository) { + return "spring-data-generated-account-repository"; + } + String identity = String.join("\u0000", + framework.id(), + candidate.file().typeName(), + candidate.file().displayPath(), + candidate.candidateId(), + candidate.candidateType(), + candidate.kind(), + "findById"); + return "spring-data-generated-" + + slug(candidate.file().typeName() + "-findById") + + "-" + digest(identity).substring(0, 16); } - private String lifecycleFactId(SourceMetadataScanner.SourceFile file, SourceMetadataScanner.MethodInfo method, String kind) { - if (file.simpleTypeName().equals("SecurityConfig") && method.name().equals("securityFilterChain")) { + private static String dynamicFeatureIdentity(DynamicFeature feature) { + return String.join("\u0000", + feature.kind().name(), + feature.caller().owner(), + feature.caller().name(), + feature.caller().descriptor(), + feature.target(), + feature.targetDescriptor(), + feature.sourcePath(), + Integer.toString(feature.sourceLine()), + Integer.toString(feature.sourceOffset()), + feature.symbols().toString()); + } + + private static String dynamicTargetSimpleName(String target) { + int separator = target.indexOf('#'); + String owner = separator < 0 ? target : target.substring(0, separator); + return simpleName(owner); + } + + private static String dynamicTargetMethod(String target) { + int separator = target.indexOf('#'); + return separator < 0 ? "" : target.substring(separator + 1); + } + + private static String uniqueFactId(Set ids, String baseId, String identity) { + if (ids.add(baseId)) { + return baseId; + } + String suffix = digest(identity).substring(0, 16); + String candidate = baseId + "-" + suffix; + int duplicate = 2; + while (!ids.add(candidate)) { + candidate = baseId + "-" + suffix + "-" + duplicate++; + } + return candidate; + } + + private static Map dynamicProxyEvidenceAttributes(DynamicFeature feature) { + if (feature.targetDescriptor().isBlank()) { + return Map.of("caller", feature.caller().displayName()); + } + return Map.of( + "caller", feature.caller().displayName(), + "targetDescriptor", feature.targetDescriptor()); + } + + private String lifecycleFactId( + SourceMetadataScanner scanner, + SourceMetadataScanner.SourceFile file, + SourceMetadataScanner.MethodInfo method, + String kind) { + if (file.simpleTypeName().equals("SecurityConfig") + && method.name().equals("securityFilterChain") + && uniqueLifecycleMethod(scanner, file, method)) { return "spring-bean-factory-to-security-config"; } - if (file.simpleTypeName().equals("DataSeeder") && method.name().equals("run")) { + if (file.simpleTypeName().equals("DataSeeder") + && method.name().equals("run") + && uniqueLifecycleMethod(scanner, file, method)) { return "spring-boot-to-data-seeder"; } - return "spring-lifecycle-" + kind + "-" + slug(file.simpleTypeName() + "-" + method.name()); + String identity = String.join("\u0000", + framework.id(), + kind, + file.typeName(), + file.displayPath(), + method.name(), + method.parameters().replaceAll("\\s+", " ").trim(), + method.returnType(), + Integer.toString(method.start())); + return "spring-lifecycle-" + kind + "-" + + slug(file.typeName() + "-" + method.name()) + + "-" + digest(identity).substring(0, 16); + } + + private static boolean uniqueLifecycleMethod( + SourceMetadataScanner scanner, + SourceMetadataScanner.SourceFile file, + SourceMetadataScanner.MethodInfo method) { + return scanner.files().stream() + .filter(candidate -> candidate.simpleTypeName().equals(file.simpleTypeName())) + .flatMap(candidate -> SourceMetadataScanner.methods(candidate).stream()) + .filter(candidate -> candidate.name().equals(method.name())) + .count() == 1; } private static String simpleName(String value) { diff --git a/src/main/java/com/projectlightpath/analysis/java/DynamicFeature.java b/src/main/java/com/projectlightpath/analysis/java/DynamicFeature.java index 45dc18c..2869e7f 100644 --- a/src/main/java/com/projectlightpath/analysis/java/DynamicFeature.java +++ b/src/main/java/com/projectlightpath/analysis/java/DynamicFeature.java @@ -9,7 +9,34 @@ public record DynamicFeature( String target, String sourcePath, List symbols, - String limitation) { + String limitation, + int sourceLine, + int sourceOffset, + String targetDescriptor) { + /** Compatibility constructor for callers that provide source-site coordinates only. */ + public DynamicFeature( + Kind kind, + MethodSymbol caller, + String target, + String sourcePath, + List symbols, + String limitation, + int sourceLine, + int sourceOffset) { + this(kind, caller, target, sourcePath, symbols, limitation, sourceLine, sourceOffset, ""); + } + + /** Compatibility constructor for provider callers that do not have source-site coordinates. */ + public DynamicFeature( + Kind kind, + MethodSymbol caller, + String target, + String sourcePath, + List symbols, + String limitation) { + this(kind, caller, target, sourcePath, symbols, limitation, 0, 0, ""); + } + public enum Kind { REFLECTION, DYNAMIC_PROXY @@ -26,5 +53,8 @@ public enum Kind { limitation = limitation == null || limitation.isBlank() ? "dynamic target is not uniquely identified by the selected context" : limitation; + sourceLine = Math.max(0, sourceLine); + sourceOffset = Math.max(0, sourceOffset); + targetDescriptor = targetDescriptor == null ? "" : targetDescriptor; } } diff --git a/src/main/java/com/projectlightpath/analysis/java/JavaAnalysisAdapter.java b/src/main/java/com/projectlightpath/analysis/java/JavaAnalysisAdapter.java index 5f525d0..807ebde 100644 --- a/src/main/java/com/projectlightpath/analysis/java/JavaAnalysisAdapter.java +++ b/src/main/java/com/projectlightpath/analysis/java/JavaAnalysisAdapter.java @@ -61,7 +61,8 @@ private static List attachExactProviderIdentities( for (SemanticCallFacts.Fact frameworkFact : frameworkFacts) { if (frameworkFact.derivedFrom() != null) { boolean providerExists = providerFacts.stream() - .anyMatch(provider -> provider.id().equals(frameworkFact.derivedFrom())); + .anyMatch(provider -> provider.id().equals(frameworkFact.derivedFrom()) + && provider.certainty() != SemanticCallFacts.Certainty.UNRESOLVED); result.add(providerExists ? frameworkFact : frameworkFact.withDerivedFrom(null)); @@ -120,7 +121,7 @@ private static List links( continue; } SemanticCallFacts.Fact provider = providers.get(frameworkFact.derivedFrom()); - if (provider != null) { + if (provider != null && provider.certainty() != SemanticCallFacts.Certainty.UNRESOLVED) { result.add(new SemanticCallFacts.Link( frameworkFact.id(), provider.id(), diff --git a/src/main/java/com/projectlightpath/analysis/java/MethodSymbol.java b/src/main/java/com/projectlightpath/analysis/java/MethodSymbol.java index e2c2195..a8f2744 100644 --- a/src/main/java/com/projectlightpath/analysis/java/MethodSymbol.java +++ b/src/main/java/com/projectlightpath/analysis/java/MethodSymbol.java @@ -1,6 +1,9 @@ package com.projectlightpath.analysis.java; import java.util.Objects; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** Stable method identity retained alongside the compact owner#name display. */ public record MethodSymbol(String owner, String name, String descriptor) { @@ -24,19 +27,106 @@ public static MethodSymbol fromSootSignature(String signature) { if (body.startsWith("<") && body.endsWith(">")) { body = body.substring(1, body.length() - 1); } - int separator = body.indexOf(": "); + int separator = body.indexOf(':'); if (separator < 0) { throw new IllegalArgumentException("invalid Soot method signature: " + signature); } - String owner = body.substring(0, separator); - String method = body.substring(separator + 2); + String owner = body.substring(0, separator).trim(); + String method = body.substring(separator + 1).trim(); int open = method.indexOf('('); if (open < 0) { - return new MethodSymbol(owner, method.trim(), signature); + throw new IllegalArgumentException("invalid Soot method signature: " + signature); + } + int close = method.lastIndexOf(')'); + if (close < open) { + throw new IllegalArgumentException("invalid Soot method signature: " + signature); } String beforeParameters = method.substring(0, open).trim(); int nameStart = beforeParameters.lastIndexOf(' '); - String name = nameStart < 0 ? beforeParameters : beforeParameters.substring(nameStart + 1); - return new MethodSymbol(owner, name, signature); + if (nameStart < 0 || nameStart == beforeParameters.length() - 1) { + throw new IllegalArgumentException("invalid Soot method signature: " + signature); + } + String returnType = beforeParameters.substring(0, nameStart).trim(); + String name = beforeParameters.substring(nameStart + 1).trim(); + String parameters = method.substring(open + 1, close).trim(); + String descriptor = "(" + splitTypes(parameters).stream() + .map(MethodSymbol::jvmType) + .reduce(new StringBuilder(), StringBuilder::append, StringBuilder::append) + + ")" + jvmType(returnType); + return new MethodSymbol(owner, name, descriptor); + } + + /** Converts Soot's Java-like type spelling into the JVM descriptor spelling. */ + private static String jvmType(String type) { + String normalized = eraseGenericArguments(type == null ? "" : type.trim()) + .replace("...", "[]") + .replaceAll("\\s+", ""); + if (normalized.isBlank()) { + throw new IllegalArgumentException("method type must not be blank"); + } + if (normalized.startsWith("[")) { + return normalized.replace('.', '/'); + } + int dimensions = 0; + while (normalized.endsWith("[]")) { + dimensions++; + normalized = normalized.substring(0, normalized.length() - 2); + } + String primitive = Map.of( + "boolean", "Z", + "byte", "B", + "char", "C", + "short", "S", + "int", "I", + "long", "J", + "float", "F", + "double", "D", + "void", "V").get(normalized); + String base = primitive != null + ? primitive + : normalized.startsWith("L") && normalized.endsWith(";") + ? normalized.replace('.', '/') + : "L" + normalized.replace('.', '/') + ";"; + return "[".repeat(dimensions) + base; + } + + /** Splits a Soot parameter list without splitting nested generic arguments. */ + private static List splitTypes(String parameters) { + if (parameters.isBlank()) { + return List.of(); + } + List result = new ArrayList<>(); + int start = 0; + int angleDepth = 0; + for (int index = 0; index < parameters.length(); index++) { + char character = parameters.charAt(index); + if (character == '<') { + angleDepth++; + } else if (character == '>') { + angleDepth = Math.max(0, angleDepth - 1); + } else if (character == ',' && angleDepth == 0) { + result.add(parameters.substring(start, index).trim()); + start = index + 1; + } + } + result.add(parameters.substring(start).trim()); + return List.copyOf(result); + } + + /** Erases Java-like generic arguments from a Soot type spelling. */ + private static String eraseGenericArguments(String value) { + StringBuilder result = new StringBuilder(value.length()); + int depth = 0; + for (int index = 0; index < value.length(); index++) { + char character = value.charAt(index); + if (character == '<') { + depth++; + } else if (character == '>') { + depth = Math.max(0, depth - 1); + } else if (depth == 0) { + result.append(character); + } + } + return result.toString(); } } diff --git a/src/main/java/com/projectlightpath/analysis/java/ProviderFactNormalizer.java b/src/main/java/com/projectlightpath/analysis/java/ProviderFactNormalizer.java index b93d16b..d4a96f1 100644 --- a/src/main/java/com/projectlightpath/analysis/java/ProviderFactNormalizer.java +++ b/src/main/java/com/projectlightpath/analysis/java/ProviderFactNormalizer.java @@ -1,7 +1,13 @@ package com.projectlightpath.analysis.java; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; /** Converts provider graph observations into provider-only SemanticCallFacts. */ final class ProviderFactNormalizer { @@ -9,10 +15,21 @@ private ProviderFactNormalizer() {} static List normalize(AnalysisContext context, AnalysisGraph graph) { List facts = new ArrayList<>(); + Set ids = new HashSet<>(); for (CallEdge edge : graph.calls()) { - String id = "sootup-call-" + Integer.toHexString( + String callIdentity = edge.caller().displayName() + "|" + edge.caller().descriptor() + + "->" + edge.callee().displayName() + "|" + edge.callee().descriptor() + + "@" + edge.line() + "|" + edge.sourcePath() + "|" + edge.relationshipType(); + String baseId = "sootup-call-" + Integer.toHexString( (edge.caller().descriptor() + "->" + edge.callee().descriptor() + "@" + edge.line()).hashCode()); - String limitation = edge.certaintyHint() == SemanticCallFacts.Certainty.POSSIBLE + String id = uniqueId(ids, baseId, callIdentity); + boolean openWorldDynamic = isOpenWorldDynamic(edge.relationshipType()); + SemanticCallFacts.Certainty certainty = openWorldDynamic + ? SemanticCallFacts.Certainty.UNRESOLVED + : edge.certaintyHint(); + String limitation = openWorldDynamic + ? "Open-world dynamic behavior does not identify a unique runtime target." + : certainty == SemanticCallFacts.Certainty.POSSIBLE ? "CHA exposes a bounded implementation/callback candidate; runtime selection is not unique." : null; facts.add(SemanticCallFacts.Fact.provider( @@ -21,7 +38,7 @@ static List normalize(AnalysisContext context, AnalysisG edge.caller().displayName(), edge.callee().displayName(), edge.relationshipType(), - edge.certaintyHint(), + certainty, List.of(SemanticCallFacts.Evidence.sootup( edge.sourcePath(), edge.line(), @@ -32,8 +49,12 @@ static List normalize(AnalysisContext context, AnalysisG } for (DynamicFeature feature : graph.dynamicFeatures()) { String relationship = feature.kind() == DynamicFeature.Kind.REFLECTION ? "reflection" : "proxy_dispatch"; - String id = "sootup-" + feature.kind().name().toLowerCase() + "-" + Integer.toHexString( - (feature.caller().descriptor() + "->" + feature.target()).hashCode()); + String baseId = dynamicFeatureId(feature); + String id = baseId; + int duplicate = 1; + while (!ids.add(id)) { + id = baseId + "-" + duplicate++; + } facts.add(SemanticCallFacts.Fact.provider( id, context, @@ -45,10 +66,77 @@ static List normalize(AnalysisContext context, AnalysisG feature.kind() == DynamicFeature.Kind.REFLECTION ? "reflection_api" : "dynamic_proxy_call", feature.sourcePath(), feature.symbols(), - null, - java.util.Map.of("basis", "SootUp graph does not contain a unique dynamic target"))), + feature.sourceLine() > 0 ? feature.sourceLine() : null, + dynamicEvidenceAttributes(feature))), feature.limitation())); } return List.copyOf(facts); } + + /** Uses the complete source identity so separate dynamic sites cannot silently collide. */ + private static String dynamicFeatureId(DynamicFeature feature) { + String identity = feature.kind().name() + "|" + + feature.sourcePath() + "|" + + feature.caller().descriptor() + "|" + + feature.target() + "|" + + feature.targetDescriptor() + "|" + + feature.sourceLine() + "|" + + feature.sourceOffset() + "|" + + feature.symbols(); + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] bytes = digest.digest(identity.getBytes(StandardCharsets.UTF_8)); + StringBuilder hex = new StringBuilder(bytes.length * 2); + for (byte value : bytes) { + hex.append(String.format("%02x", value)); + } + return "sootup-" + feature.kind().name().toLowerCase() + "-" + hex; + } catch (NoSuchAlgorithmException error) { + throw new IllegalStateException("SHA-256 is unavailable", error); + } + } + + /** Keeps legacy IDs stable for ordinary edges while making collisions explicit and safe. */ + private static String uniqueId(Set ids, String baseId, String identity) { + if (ids.add(baseId)) { + return baseId; + } + String digest = sha256(identity); + String candidate = baseId + "-" + digest; + int duplicate = 1; + while (!ids.add(candidate)) { + candidate = baseId + "-" + digest + "-" + duplicate++; + } + return candidate; + } + + private static Map dynamicEvidenceAttributes(DynamicFeature feature) { + if (feature.targetDescriptor().isBlank()) { + return Map.of("basis", "SootUp graph does not contain a unique dynamic target"); + } + return Map.of( + "basis", "SootUp graph does not contain a unique dynamic target", + "targetDescriptor", feature.targetDescriptor()); + } + + private static String sha256(String value) { + try { + byte[] bytes = MessageDigest.getInstance("SHA-256") + .digest(value.getBytes(StandardCharsets.UTF_8)); + StringBuilder hex = new StringBuilder(bytes.length * 2); + for (byte item : bytes) { + hex.append(String.format("%02x", item)); + } + return hex.toString(); + } catch (NoSuchAlgorithmException error) { + throw new IllegalStateException("SHA-256 is unavailable", error); + } + } + + /** Keeps open-world reflection and proxy edges abstained even if a provider overstates certainty. */ + private static boolean isOpenWorldDynamic(String relationshipType) { + return "reflection".equals(relationshipType) + || "dynamic_proxy".equals(relationshipType) + || "proxy_dispatch".equals(relationshipType); + } } diff --git a/src/main/java/com/projectlightpath/analysis/java/SootUpBaseGraphProvider.java b/src/main/java/com/projectlightpath/analysis/java/SootUpBaseGraphProvider.java index 6bb0714..7c59532 100644 --- a/src/main/java/com/projectlightpath/analysis/java/SootUpBaseGraphProvider.java +++ b/src/main/java/com/projectlightpath/analysis/java/SootUpBaseGraphProvider.java @@ -13,6 +13,7 @@ import sootup.callgraph.CallGraph; import sootup.callgraph.ClassHierarchyAnalysisAlgorithm; import sootup.core.inputlocation.AnalysisInputLocation; +import sootup.core.jimple.common.expr.JInterfaceInvokeExpr; import sootup.core.model.SootMethod; import sootup.core.model.SourceType; import sootup.core.signatures.MethodSignature; @@ -104,17 +105,20 @@ private static List applicationCalls( continue; } String sourcePath = sourcePath(context, caller.owner()); - String source = sourceFor(scanner, caller.owner()); - boolean serviceLoaderCall = source.contains("ServiceLoader.load") - || (caller.owner().endsWith("ServiceLoaderBridge") && caller.name().startsWith("lambda$")); - String relationshipType = serviceLoaderCall ? "service_loader" - : caller.name().startsWith("lambda$") ? "callback" : "invokes"; - boolean boundedDispatch = scanner.files().stream() - .filter(file -> file.typeName().equals(callee.owner())) - .anyMatch(file -> file.source().matches("(?s).*\\bimplements\\s+[^\\{]+.*")); - boolean sourceNamesConcreteTarget = source.contains(simpleName(callee.owner())); + String methodSource = scanner.methodSource(caller, call.getLineNumber()).orElse(""); + boolean serviceLoaderCall = scanner.sourceLineContains( + caller, call.getLineNumber(), "ServiceLoader.load") + || (call.getLineNumber() <= 0 && methodSource.contains("ServiceLoader.load")); + boolean interfaceDispatch = scanner.isInterfaceType(callee.owner()) + || call.invokableStmt().getInvokeExpr() + .map(expression -> expression instanceof JInterfaceInvokeExpr) + .orElse(false); + String relationshipType = serviceLoaderCall + ? "service_loader" + : interfaceDispatch + ? "interface_dispatch" + : caller.name().startsWith("lambda$") ? "callback" : "invokes"; SemanticCallFacts.Certainty certainty = relationshipType.equals("invokes") - && !(boundedDispatch && !sourceNamesConcreteTarget) ? SemanticCallFacts.Certainty.DEFINITE : SemanticCallFacts.Certainty.POSSIBLE; result.add(new CallEdge( @@ -129,11 +133,6 @@ private static List applicationCalls( return List.copyOf(result); } - private static String simpleName(String typeName) { - int dot = typeName.lastIndexOf('.'); - return dot < 0 ? typeName : typeName.substring(dot + 1); - } - private static String sourcePath(AnalysisContext context, String owner) { String topLevel = owner.contains("$") ? owner.substring(0, owner.indexOf('$')) : owner; String relative = topLevel.replace('.', '/') + ".java"; @@ -146,12 +145,4 @@ private static String sourcePath(AnalysisContext context, String owner) { return "unknown"; } - private static String sourceFor(SourceMetadataScanner scanner, String owner) { - String topLevel = owner.contains("$") ? owner.substring(0, owner.indexOf('$')) : owner; - return scanner.files().stream() - .filter(file -> file.typeName().equals(topLevel)) - .map(SourceMetadataScanner.SourceFile::source) - .findFirst() - .orElse(""); - } } diff --git a/src/main/java/com/projectlightpath/analysis/java/SourceMetadataScanner.java b/src/main/java/com/projectlightpath/analysis/java/SourceMetadataScanner.java index ef2bcb2..72ec4b5 100644 --- a/src/main/java/com/projectlightpath/analysis/java/SourceMetadataScanner.java +++ b/src/main/java/com/projectlightpath/analysis/java/SourceMetadataScanner.java @@ -5,9 +5,12 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -26,14 +29,83 @@ final class SourceMetadataScanner { private static final Pattern PACKAGE = Pattern.compile("\\bpackage\\s+([\\w.]+)\\s*;"); private static final Pattern TYPE = Pattern.compile("\\b(?:class|interface|record|enum)\\s+(\\w+)"); + private static final Pattern TYPE_DECLARATION = Pattern.compile( + "\\b(class|interface|record|enum)\\s+(\\w+)"); + private static final Pattern NEW_EXPRESSION = Pattern.compile("\\bnew\\b"); private static final Pattern METHOD = Pattern.compile( "(?m)(?:^|[;{}\\n])\\s*(?:(?:@[A-Za-z_][\\w.]*(?:\\([^)]*\\))?\\s*)|(?:(?:public|protected|private|static|final|synchronized|abstract|native|default|strictfp)\\s+))*(?:<[^>{}]+>\\s+)?[\\w.?$<>\\[\\], ]+\\s+(\\w+)\\s*\\(([^)]*)\\)\\s*(?:throws [^{;]+)?(?:\\{|;)"); private static final Pattern QUOTED = Pattern.compile("\\\"([^\\\"]*)\\\""); - private static final Pattern CLASS_FOR_NAME = Pattern.compile("Class\\.forName\\s*\\(\\s*\\\"([^\\\"]+)\\\""); - private static final Pattern DECLARED_METHOD = Pattern.compile("getDeclaredMethod\\s*\\(\\s*\\\"([^\\\"]+)\\\""); + private static final Pattern CLASS_FOR_NAME = Pattern.compile( + "(?]*>\\s*\\[\\s*\\]\\s*\\{([^}]*)\\}"); private static final Pattern CLASS_LITERAL = Pattern.compile("([A-Za-z_][\\w.]*)\\.class"); + private static final Pattern PROXY_CALL = Pattern.compile( + "(? JAVA_LANG_TYPES = Set.of( "Appendable", "AutoCloseable", "Boolean", "Byte", "Character", "CharSequence", "Class", "Cloneable", "Comparable", "Double", "Enum", "Exception", "Float", - "Integer", "Iterable", "Long", "Math", "Number", "Object", "Record", "Runnable", + "ClassLoader", "Integer", "Iterable", "Long", "Math", "Number", "Object", "Record", "Runnable", "RuntimeException", "Short", "StrictMath", "String", "Thread", "Throwable", "Void"); + private static final Map KNOWN_CLASSLOADER_SUPERTYPES = Map.of( + "java.net.URLClassLoader", "java.security.SecureClassLoader", + "java.security.SecureClassLoader", "java.lang.ClassLoader", + "jdk.internal.loader.BuiltinClassLoader", "java.security.SecureClassLoader", + "jdk.internal.loader.ClassLoaders$AppClassLoader", "jdk.internal.loader.BuiltinClassLoader", + "jdk.internal.loader.ClassLoaders$PlatformClassLoader", "jdk.internal.loader.BuiltinClassLoader"); + private static final Set KNOWN_JDK_TYPE_NAMES = Set.of( + "java.lang.Class", + "java.lang.ClassLoader", + "java.lang.reflect.Method", + "java.lang.reflect.Constructor", + "java.lang.reflect.InvocationHandler", + "java.lang.reflect.Proxy", + "java.net.URLClassLoader", + "java.security.SecureClassLoader"); private static final Set JAVA_PRIMITIVE_TYPES = Set.of( "boolean", "byte", "char", "short", "int", "long", "float", "double", "void"); private static final Set CDI_PRODUCER_ANNOTATIONS = Set.of( @@ -105,6 +192,156 @@ List files() { return files; } + /** Returns whether a source declaration is an interface in the selected roots. */ + boolean isInterfaceType(String typeName) { + return declaredType(typeName) + .map(type -> type.kind().equals("interface")) + .orElse(false); + } + + /** Returns whether a source class declares an interface implementation contract. */ + boolean implementsInterface(String typeName) { + for (SourceFile file : files()) { + for (TypeInfo type : typeDeclarations(file)) { + if (!typeContaining(file, type.start()).equals(typeName) || !type.kind().equals("class")) { + continue; + } + String source = maskStringLiterals(file.structuralSource()); + int opening = typeBodyOpening(source, type.start()); + if (opening >= 0 && Pattern.compile("\\bimplements\\b") + .matcher(source.substring(type.start(), opening)).find()) { + return true; + } + } + } + return false; + } + + /** Returns the source body containing one provider call site. */ + Optional methodSource(MethodSymbol symbol, int line) { + SourceFile file = sourceFile(symbol.owner()).orElse(null); + if (file == null) { + return Optional.empty(); + } + int lineOffset = line > 0 ? lineStartOffset(file.source(), line) : -1; + List candidates = executableMethods(file).stream() + .filter(method -> method.name().equals(symbol.name())) + .filter(method -> typeContaining(file, method.start()).equals(symbol.owner())) + .toList(); + MethodInfo selected = candidates.stream() + .filter(method -> lineOffset >= 0 && methodContains(file, method, lineOffset)) + .findFirst() + .orElseGet(() -> candidates.stream() + .filter(method -> descriptorParameterCount(symbol.descriptor()) < 0 + || parameters(method.parameters()).size() == descriptorParameterCount(symbol.descriptor())) + .findFirst() + .orElse(null)); + if (selected == null && lineOffset >= 0) { + MethodInfo enclosing = methodContaining(file, lineOffset); + if (!enclosing.name().equals("")) { + selected = enclosing; + } + } + if (selected == null) { + return Optional.empty(); + } + int bodyEnd = selected.end(); + String source = file.structuralSource(); + if (bodyEnd > 0 && bodyEnd <= source.length() && source.charAt(bodyEnd - 1) == '{') { + int closing = matchingDelimiter(source, bodyEnd - 1, '{', '}'); + if (closing >= 0) { + bodyEnd = closing + 1; + } + } + return Optional.of(source.substring(selected.start(), Math.min(bodyEnd, source.length()))); + } + + /** Checks a token on the source line attributed to a provider call. */ + boolean sourceLineContains(MethodSymbol symbol, int line, String token) { + if (line <= 0) { + return false; + } + return sourceFile(symbol.owner()) + .map(file -> { + String source = maskStringLiterals(file.structuralSource()); + int start = lineStartOffset(source, line); + if (start < 0) { + return false; + } + int end = source.indexOf('\n', start); + String sourceLine = source.substring(start, end < 0 ? source.length() : end); + return sourceLine.contains(token); + }) + .orElse(false); + } + + private Optional declaredType(String typeName) { + return sourceFile(typeName).flatMap(file -> typeDeclarations(file).stream() + .filter(type -> typeContaining(file, type.start()).equals(typeName)) + .findFirst()); + } + + private Optional sourceFile(String owner) { + String topLevel = owner.contains("$") ? owner.substring(0, owner.indexOf('$')) : owner; + return files().stream().filter(file -> file.typeName().equals(topLevel)).findFirst(); + } + + private static boolean methodContains(SourceFile file, MethodInfo method, int offset) { + String source = file.structuralSource(); + if (method.start() > offset || method.end() <= 0 || method.end() > source.length() + || source.charAt(method.end() - 1) != '{') { + return false; + } + int closing = matchingDelimiter(source, method.end() - 1, '{', '}'); + return closing >= offset; + } + + private static int lineStartOffset(String source, int line) { + if (line <= 0) { + return -1; + } + int currentLine = 1; + for (int index = 0; index < source.length(); index++) { + if (currentLine == line) { + return index; + } + if (source.charAt(index) == '\n') { + currentLine++; + } + } + return currentLine == line ? source.length() : -1; + } + + private static int descriptorParameterCount(String descriptor) { + if (descriptor == null || descriptor.isBlank() || descriptor.charAt(0) != '(') { + return -1; + } + int close = descriptor.indexOf(')'); + if (close < 0) { + return -1; + } + int count = 0; + for (int index = 1; index < close;) { + while (index < close && descriptor.charAt(index) == '[') { + index++; + } + if (index >= close) { + return -1; + } + if (descriptor.charAt(index) == 'L') { + int end = descriptor.indexOf(';', index); + if (end < 0 || end > close) { + return -1; + } + index = end + 1; + } else { + index++; + } + count++; + } + return count; + } + /** Resolves a source type using explicit imports, the declaring package, or java.lang. */ Optional resolveReferenceType(String type, SourceFile sourceFile) { String normalized = stripBalancedGenericArguments(type == null ? "" : type).trim(); @@ -189,7 +426,7 @@ private static List importedTypes(String type, SourceFile sourceFile) { simpleType = type.substring(0, nestedSeparator); } Matcher matcher = Pattern.compile("(?m)^\\s*import\\s+([A-Za-z_][\\w.]*)\\s*;") - .matcher(sourceFile.structuralSource()); + .matcher(maskStringLiterals(sourceFile.structuralSource())); List result = new ArrayList<>(); while (matcher.find()) { String imported = matcher.group(1); @@ -208,7 +445,7 @@ private List onDemandImportedTypes(String type, SourceFile sourceFile) { simpleType = type.substring(0, nestedSeparator); } Matcher matcher = Pattern.compile("(?m)^\\s*import\\s+([A-Za-z_][\\w.]*)\\.\\*\\s*;") - .matcher(sourceFile.structuralSource()); + .matcher(maskStringLiterals(sourceFile.structuralSource())); Set packages = new LinkedHashSet<>(); while (matcher.find()) { packages.add(matcher.group(1)); @@ -223,6 +460,27 @@ private List onDemandImportedTypes(String type, SourceFile sourceFile) { .toList(); } + private static List onDemandImportedJdkTypes(String type, SourceFile sourceFile) { + String simpleType = type; + int nestedSeparator = type.indexOf('.'); + if (nestedSeparator >= 0) { + simpleType = type.substring(0, nestedSeparator); + } + Matcher matcher = Pattern.compile("(?m)^\\s*import\\s+([A-Za-z_][\\w.]*)\\.\\*\\s*;") + .matcher(maskStringLiterals(sourceFile.structuralSource())); + Set packages = new LinkedHashSet<>(); + while (matcher.find()) { + packages.add(matcher.group(1)); + } + String suffix = type.substring(simpleType.length()); + String requestedSimpleType = simpleType; + return KNOWN_JDK_TYPE_NAMES.stream() + .filter(candidate -> packages.contains(candidate.substring(0, candidate.lastIndexOf('.')))) + .filter(candidate -> candidate.substring(candidate.lastIndexOf('.') + 1).equals(requestedSimpleType)) + .map(candidate -> candidate + suffix) + .toList(); + } + /** Scans bounded Quarkus and Spring route annotations. */ List routes(AnalysisContext.Framework framework) { List result = new ArrayList<>(); @@ -518,64 +776,2449 @@ List generatedMethods() { List dynamicFeatures() { List result = new ArrayList<>(); for (SourceFile file : files()) { - String source = file.source(); - if (source.contains("Class.forName") || source.contains("getDeclaredMethod") || source.contains("Method.invoke")) { - Matcher targetClass = CLASS_FOR_NAME.matcher(source); - Matcher targetMethod = DECLARED_METHOD.matcher(source); - String target = (targetClass.find() ? targetClass.group(1) : "unknown") - + "#" + (targetMethod.find() ? targetMethod.group(1) : "unknown"); - MethodInfo caller = methodContaining(file, firstDynamicIndex(source)); + for (DynamicSite site : reflectionSites(file)) { + MethodInfo caller = methodContaining(file, site.offset()); result.add(new DynamicFeature( DynamicFeature.Kind.REFLECTION, - new MethodSymbol(file.typeName(), caller.name(), ""), - target, + new MethodSymbol(typeContaining(file, site.offset()), caller.name(), ""), + site.target(), file.displayPath(), - List.of("Class.forName", "getDeclaredMethod", "Method.invoke"), - "Class.forName, Method.getDeclaredMethod, and Method.invoke do not identify a unique runtime target.")); - } - if (source.contains("Proxy.newProxyInstance") || source.contains("InvocationHandler")) { - Matcher interfaces = PROXY_INTERFACES.matcher(source); - List interfaceNames = new ArrayList<>(); - if (interfaces.find()) { - Matcher literal = CLASS_LITERAL.matcher(interfaces.group(1)); - while (literal.find()) { - interfaceNames.add(literal.group(1)); - } - } - MethodInfo caller = methodContaining(file, firstDynamicIndex(source)); + List.of(site.symbol()), + "Open-world reflection APIs do not identify a unique runtime target.", + file.lineAt(site.offset()), + site.offset())); + } + String dynamicSource = maskStringLiterals(file.structuralSource()); + for (int proxyOffset : proxyCallOffsets(file, dynamicSource)) { + List interfaceNames = proxyInterfaceNames(file.structuralSource(), proxyOffset); + MethodInfo caller = methodContaining(file, proxyOffset); if (interfaceNames.isEmpty()) { - result.add(dynamicProxyFeature(file, caller, "unknown#unknown")); + result.add(dynamicProxyFeature(file, caller, "unknown#unknown", "", proxyOffset)); } else { for (String interfaceName : interfaceNames) { - String qualified = qualify(interfaceName, file.packageName()); + String qualified = resolveContractType(interfaceName, file) + .orElseGet(() -> qualify(interfaceName, file.packageName())); Optional interfaceFile = files().stream() - .filter(candidate -> candidate.typeName().equals(qualified) - || candidate.simpleTypeName().equals(interfaceName)) + .filter(candidate -> candidate.typeName().equals(qualified)) .findFirst(); List interfaceMethods = interfaceFile.map(SourceMetadataScanner::methods).orElse(List.of()); if (interfaceMethods.isEmpty()) { - result.add(dynamicProxyFeature(file, caller, qualified + "#unknown")); + result.add(dynamicProxyFeature(file, caller, qualified + "#unknown", "", proxyOffset)); } else { for (MethodInfo method : interfaceMethods) { - result.add(dynamicProxyFeature(file, caller, qualified + "#" + method.name())); + result.add(dynamicProxyFeature( + file, + caller, + qualified + "#" + method.name(), + methodDescriptor(method), + proxyOffset)); } } } } } } - return List.copyOf(result); + return List.copyOf(result); + } + + /** Finds qualified proxy calls and explicitly statically imported unqualified calls. */ + private List proxyCallOffsets(SourceFile file, String source) { + List result = new ArrayList<>(); + Matcher qualified = PROXY_CALL.matcher(source); + while (qualified.find()) { + if (isBareReceiver(source, qualified.start(1)) + && (isJdkApiOwner(file, qualified.group(1), qualified.start(1), "java.lang.reflect.Proxy") + || isDeclaredVariableBefore(file, "Proxy", qualified.group(1), qualified.start(1)))) { + result.add(qualified.start()); + } + } + Matcher thisQualified = THIS_PROXY_CALL.matcher(source); + while (thisQualified.find()) { + if (isBareReceiver(source, thisQualified.start()) + && hasVisibleField(file, "Proxy", thisQualified.group(1), thisQualified.start(1))) { + result.add(thisQualified.start()); + } + } + Matcher qualifiedThis = QUALIFIED_THIS_PROXY_CALL.matcher(source); + while (qualifiedThis.find()) { + String owner = qualifiedThisOwner(file, qualifiedThis.group(1), qualifiedThis.start(1)); + if (owner != null + && hasVisibleField(file, "Proxy", qualifiedThis.group(2), qualifiedThis.start(2), owner)) { + result.add(qualifiedThis.start()); + } + } + if (STATIC_PROXY_IMPORT.matcher(source).find()) { + Matcher unqualified = UNQUALIFIED_PROXY_CALL.matcher(source); + while (unqualified.find()) { + int prefix = unqualified.start() - 1; + while (prefix >= 0 && Character.isWhitespace(source.charAt(prefix))) { + prefix--; + } + if ((prefix < 0 || source.charAt(prefix) != '.') + && !hasVisibleMethodNamed(file, unqualified.start(), "newProxyInstance")) { + result.add(unqualified.start()); + } + } + } + return result.stream().distinct().sorted().toList(); + } + + /** Finds the interface array associated with one proxy construction site. */ + private static List proxyInterfaceNames(String source, int proxyOffset) { + int opening = source.indexOf('(', proxyOffset); + if (opening < 0) { + return List.of(); + } + int closing = matchingDelimiter(source, opening, '(', ')'); + if (closing < 0) { + return List.of(); + } + List arguments = splitTopLevelArguments(source.substring(opening + 1, closing)); + if (arguments.size() < 2) { + return List.of(); + } + Matcher interfaces = PROXY_INTERFACES.matcher(arguments.get(1)); + if (!interfaces.find()) { + return List.of(); + } + List result = new ArrayList<>(); + Matcher literal = CLASS_LITERAL.matcher(interfaces.group(1)); + while (literal.find()) { + result.add(literal.group(1)); + } + return List.copyOf(result); + } + + /** Splits one call's arguments without crossing nested expressions. */ + private static List splitTopLevelArguments(String arguments) { + List result = new ArrayList<>(); + int start = 0; + int parentheses = 0; + int brackets = 0; + int braces = 0; + int angleBrackets = 0; + boolean quoted = false; + boolean escaped = false; + for (int index = 0; index < arguments.length(); index++) { + char character = arguments.charAt(index); + if (character == '"' && !escaped) { + quoted = !quoted; + } + if (quoted) { + escaped = character == '\\' && !escaped; + if (character != '\\') { + escaped = false; + } + continue; + } + escaped = false; + switch (character) { + case '(' -> parentheses++; + case ')' -> parentheses--; + case '[' -> brackets++; + case ']' -> brackets--; + case '{' -> braces++; + case '}' -> braces--; + case '<' -> { + if (parentheses == 0 && brackets == 0 && braces == 0) { + angleBrackets++; + } + } + case '>' -> { + if (parentheses == 0 && brackets == 0 && braces == 0) { + angleBrackets = Math.max(0, angleBrackets - 1); + } + } + case ',' -> { + if (parentheses == 0 && brackets == 0 && braces == 0 && angleBrackets == 0) { + result.add(arguments.substring(start, index).trim()); + start = index + 1; + } + } + default -> { } + } + } + result.add(arguments.substring(start).trim()); + return result; + } + + /** Finds each executable reflection marker without treating strings or comments as code. */ + private List reflectionSites(SourceFile file) { + String source = file.structuralSource(); + String dynamicSource = maskStringLiterals(source); + List result = new ArrayList<>(); + addClassForNameSites(result, dynamicSource, source, file); + if (STATIC_FOR_NAME_IMPORT.matcher(dynamicSource).find()) { + addStaticForNameSites(result, dynamicSource, source, file); + } + addReceiverCallSites(result, dynamicSource, source, file, RECEIVER_LOAD_CLASS, "ClassLoader"); + addReceiverCallSites(result, dynamicSource, source, file, RECEIVER_METHOD_LOOKUP, "Class"); + addReceiverCallSites(result, dynamicSource, source, file, RECEIVER_CONSTRUCTOR_LOOKUP, "Class"); + List supportedChainedReceivers = chainedReceivers(dynamicSource).stream() + .filter(receiver -> isSupportedChainedReceiver(file, dynamicSource, receiver)) + .toList(); + addChainedLookupSites( + result, dynamicSource, source, supportedChainedReceivers, + "getDeclaredMethod", "getMethod", "getDeclaredConstructor", "getConstructor"); + addJdkOwnerSites(result, dynamicSource, source, file, CONSTRUCTOR_NEW_INSTANCE, + "Constructor.newInstance", "java.lang.reflect.Constructor"); + addJdkOwnerSites(result, dynamicSource, source, file, CLASS_NEW_INSTANCE, + "Class.newInstance", "java.lang.Class"); + + addReceiverSites(result, dynamicSource, source, file, "newInstance", "Constructor", "Constructor.newInstance"); + addReceiverSites(result, dynamicSource, source, file, "newInstance", "Class", "Class.newInstance"); + + Matcher invoke = METHOD_INVOKE.matcher(dynamicSource); + while (invoke.find()) { + if (isBareReceiver(dynamicSource, invoke.start(1)) + && (isDeclaredVariableBefore(file, "Method", invoke.group(1), invoke.start()) + || isInvocationHandlerParameterBefore(file, invoke.group(1), invoke.start()))) { + addSite(result, source, invoke.start(), "Method.invoke"); + } + } + addChainedMethodInvokeSites(result, dynamicSource, source, supportedChainedReceivers); + addChainedConstructorSites(result, dynamicSource, source, supportedChainedReceivers); + addJdkOwnerSites(result, dynamicSource, source, file, METHOD_TYPE_INVOKE, + "Method.invoke", "java.lang.reflect.Method"); + addThisReceiverSites(result, dynamicSource, source, file); + addInlineProxyInvocationSites(result, dynamicSource, source, file); + result.sort(Comparator.comparingInt(DynamicSite::offset).thenComparing(DynamicSite::symbol)); + return result; + } + + private void addJdkOwnerSites( + List result, + String dynamicSource, + String source, + SourceFile file, + Pattern pattern, + String symbol, + String expectedType) { + Matcher matcher = pattern.matcher(dynamicSource); + while (matcher.find()) { + if (isBareReceiver(dynamicSource, matcher.start(1)) + && isJdkApiOwner(file, matcher.group(1), matcher.start(1), expectedType)) { + addSite(result, source, matcher.start(), symbol); + } + } + } + + private void addClassForNameSites( + List result, + String dynamicSource, + String source, + SourceFile file) { + Matcher matcher = CLASS_FOR_NAME.matcher(dynamicSource); + while (matcher.find()) { + if (isBareReceiver(dynamicSource, matcher.start(1)) + && (isJdkApiOwner(file, matcher.group(1), matcher.start(1), "java.lang.Class") + || isDeclaredVariableBefore(file, "Class", matcher.group(1), matcher.start(1)))) { + addSite(result, source, matcher.start(), "Class.forName"); + } + } + Matcher thisQualified = THIS_CLASS_FOR_NAME.matcher(dynamicSource); + while (thisQualified.find()) { + if (isBareReceiver(dynamicSource, thisQualified.start()) + && hasVisibleField(file, "Class", thisQualified.group(1), thisQualified.start(1))) { + addSite(result, source, thisQualified.start(), "Class.forName"); + } + } + Matcher qualifiedThis = QUALIFIED_THIS_CLASS_FOR_NAME.matcher(dynamicSource); + while (qualifiedThis.find()) { + String owner = qualifiedThisOwner(file, qualifiedThis.group(1), qualifiedThis.start(1)); + if (owner != null + && hasVisibleField(file, "Class", qualifiedThis.group(2), qualifiedThis.start(2), owner)) { + addSite(result, source, qualifiedThis.start(), "Class.forName"); + } + } + } + + private void addStaticForNameSites( + List result, String dynamicSource, String source, SourceFile file) { + Matcher matcher = STATIC_FOR_NAME.matcher(dynamicSource); + while (matcher.find()) { + int prefixStart = Math.max(0, matcher.start() - 24); + String prefix = dynamicSource.substring(prefixStart, matcher.start()); + int previous = matcher.start() - 1; + while (previous >= 0 && Character.isWhitespace(dynamicSource.charAt(previous))) { + previous--; + } + if ((previous < 0 || dynamicSource.charAt(previous) != '.') + && !hasVisibleMethodNamed(file, matcher.start(), "forName")) { + addSite(result, source, matcher.start(), "Class.forName"); + } + } + } + + private boolean hasVisibleMethodNamed(SourceFile file, int offset, String name) { + String siteType = typeContaining(file, offset); + Set visibleOwners = visibleFieldOwners(file, siteType).keySet(); + return files().stream() + .flatMap(candidate -> methods(candidate).stream() + .filter(method -> method.name().equals(name)) + .filter(method -> !isDeclarationKeyword(method.returnType())) + .filter(method -> isAccessibleMethod(candidate, file, method, siteType)) + .map(method -> typeContaining(candidate, method.start()))) + .anyMatch(visibleOwners::contains); + } + + private static boolean isAccessibleMethod( + SourceFile declaringFile, SourceFile siteFile, MethodInfo method, String siteType) { + String declaration = declaringFile.structuralSource() + .substring(method.start(), Math.min(method.end(), declaringFile.structuralSource().length())); + String declaringType = typeContaining(declaringFile, method.start()); + boolean privateMethod = Pattern.compile("\\bprivate\\b").matcher(declaration).find(); + boolean lexicalAccess = siteType.equals(declaringType) + || siteType.startsWith(declaringType + "$"); + if (privateMethod) { + return lexicalAccess; + } + if (isInterfaceType(declaringFile, declaringType) + && !lexicalAccess + && Pattern.compile("\\bstatic\\b").matcher(declaration).find()) { + return false; + } + return declaringFile.packageName().equals(siteFile.packageName()) + || Pattern.compile("\\b(?:public|protected)\\b").matcher(declaration).find() + || isInterfaceType(declaringFile, declaringType); + } + + private static boolean isInterfaceType(SourceFile file, String owner) { + return typeDeclarations(file).stream() + .filter(type -> typeContaining(file, type.start()).equals(owner)) + .anyMatch(type -> type.kind().equals("interface")); + } + + private boolean startsWithJdkClassForName(SourceFile file, String source, int offset) { + Matcher owner = CLASS_FOR_NAME_PREFIX.matcher(source.substring(offset)); + return !owner.find() + || isJdkApiOwner(file, owner.group(1), offset, "java.lang.Class"); + } + + /** Confirms that a source owner resolves to the JDK API rather than an application type. */ + private boolean isJdkApiOwner( + SourceFile file, String spelling, int offset, String expectedType) { + if (spelling != null + && spelling.matches("[A-Za-z_$][\\w$]*") + && hasVisibleValueNamed(file, spelling, offset)) { + String expectedSimpleName = jdkSimpleName(expectedType); + return expectedSimpleName != null + && isDeclaredVariableBefore(file, expectedSimpleName, spelling, offset); + } + return resolveOwnerType(spelling, file, offset) + .map(expectedType::equals) + .orElse(false); + } + + private static String jdkSimpleName(String expectedType) { + return switch (expectedType) { + case "java.lang.Class" -> "Class"; + case "java.lang.ClassLoader" -> "ClassLoader"; + case "java.lang.reflect.Method" -> "Method"; + case "java.lang.reflect.Constructor" -> "Constructor"; + case "java.lang.reflect.InvocationHandler" -> "InvocationHandler"; + case "java.lang.reflect.Proxy" -> "Proxy"; + default -> null; + }; + } + + private boolean hasVisibleValueNamed(SourceFile file, String variable, int offset) { + MethodInfo owner = methodContaining(file, offset); + return (!owner.name().equals("") && hasParameter(owner, variable)) + || (!owner.name().equals("") + && activeLocalDeclaration(file, owner, variable, offset).isPresent()) + || visibleField(file, variable, offset) != null; + } + + private Optional resolveOwnerType(String spelling, SourceFile sourceFile, int offset) { + String normalized = stripBalancedGenericArguments(spelling == null ? "" : spelling).trim(); + if (normalized.isBlank()) { + return Optional.empty(); + } + if (!normalized.contains(".")) { + List lexical = lexicalTypeCandidates(normalized, sourceFile, offset); + if (!lexical.isEmpty()) { + return lexical.size() == 1 ? Optional.of(lexical.get(0)) : Optional.empty(); + } + } + List imported = importedTypes(normalized, sourceFile); + if (imported.size() == 1) { + return Optional.of(normalizeDeclaredTypeName(imported.get(0), sourceFile)); + } + List declared = normalized.contains(".") + ? declaredTypeCandidates(normalized, sourceFile, offset) + : samePackageTypeCandidates(normalized, sourceFile); + if (declared.size() == 1) { + return Optional.of(declared.get(0)); + } + if (!declared.isEmpty()) { + return Optional.empty(); + } + List onDemand = new ArrayList<>(onDemandImportedTypes(normalized, sourceFile)); + onDemand.addAll(onDemandImportedJdkTypes(normalized, sourceFile)); + onDemand = onDemand.stream().distinct().toList(); + if (onDemand.size() == 1) { + return Optional.of(normalizeDeclaredTypeName(onDemand.get(0), sourceFile)); + } + if (onDemand.size() > 1) { + return Optional.empty(); + } + if (normalized.equals("java.lang.Class") || normalized.equals("java.lang.reflect.Proxy")) { + return Optional.of(normalized); + } + if (normalized.equals("java.lang.ClassLoader") + || normalized.equals("java.lang.reflect.Method") + || normalized.equals("java.lang.reflect.Constructor") + || normalized.equals("java.lang.reflect.InvocationHandler")) { + return Optional.of(normalized); + } + if (normalized.equals("Class")) { + return Optional.of("java.lang.Class"); + } + if (normalized.equals("Proxy") && hasOnDemandImport(sourceFile, "java.lang.reflect")) { + return Optional.of("java.lang.reflect.Proxy"); + } + if (normalized.equals("Method")) { + return Optional.of("java.lang.reflect.Method"); + } + if (normalized.equals("Constructor")) { + return Optional.of("java.lang.reflect.Constructor"); + } + if (normalized.equals("ClassLoader")) { + return Optional.of("java.lang.ClassLoader"); + } + if (normalized.equals("InvocationHandler")) { + return Optional.of("java.lang.reflect.InvocationHandler"); + } + return Optional.empty(); + } + + private boolean isJdkDeclaredType( + SourceFile file, String spelling, int offset, String expectedSimpleName) { + return isJdkDeclaredType(file, spelling, offset, expectedSimpleName, new HashSet<>()); + } + + private boolean isJdkDeclaredType( + SourceFile file, + String spelling, + int offset, + String expectedSimpleName, + Set typeVariables) { + String normalized = stripBalancedGenericArguments(spelling == null ? "" : spelling).trim(); + Optional bounds = typeVariableBounds(file, normalized, offset); + if (bounds.isPresent()) { + if (!typeVariables.add(normalized)) { + return false; + } + return bounds.get().bounds().stream() + .anyMatch(bound -> isJdkDeclaredType( + file, + bound, + bounds.get().declarationOffset(), + expectedSimpleName, + new HashSet<>(typeVariables))); + } + String expectedType = resolveJdkTypeName(expectedSimpleName); + return resolveOwnerType(normalized, file, offset) + .map(resolved -> isJdkTypeOrSubclass(resolved, expectedType, new HashSet<>())) + .orElse(false); + } + + private Optional typeVariableBounds( + SourceFile file, String variable, int offset) { + if (!variable.matches("[A-Za-z_$][\\w$]*")) { + return Optional.empty(); + } + MethodInfo method = methodContaining(file, offset); + if (!method.name().equals("")) { + Optional> methodBounds = typeVariableBoundsInMethod(method, variable); + if (methodBounds.isPresent()) { + return methodBounds.map(bounds -> new TypeVariableBounds(bounds, method.start())); + } + } + List containingTypes = typeDeclarations(file).stream() + .filter(type -> type.start() <= offset && type.end() >= offset) + .sorted(Comparator.comparingInt(TypeInfo::start).reversed()) + .toList(); + for (TypeInfo type : containingTypes) { + Optional> typeBounds = typeVariableBoundsInType(file, type, variable); + if (typeBounds.isPresent()) { + return typeBounds.map(bounds -> new TypeVariableBounds(bounds, type.start())); + } + } + return Optional.empty(); + } + + private static Optional> typeVariableBoundsInMethod( + MethodInfo method, String variable) { + String header = method.annotationBlock().replaceFirst("^[;{}\\s]+", ""); + Matcher opening = Pattern.compile("<(?![=])").matcher(header); + while (opening.find()) { + String prefix = header.substring(0, opening.start()).trim(); + if (!isMethodTypeParameterPrefix(prefix)) { + continue; + } + int closing = matchingGenericDelimiter(header, opening.start()); + if (closing < 0) { + continue; + } + return typeVariableBoundsInParameters(header.substring(opening.end(), closing), variable); + } + return Optional.empty(); + } + + private static boolean isMethodTypeParameterPrefix(String prefix) { + Set modifiers = Set.of( + "public", "protected", "private", "static", "final", "synchronized", + "abstract", "native", "default", "strictfp"); + int index = 0; + while (index < prefix.length()) { + while (index < prefix.length() && Character.isWhitespace(prefix.charAt(index))) { + index++; + } + if (index >= prefix.length()) { + return true; + } + if (prefix.charAt(index) == '@') { + index++; + while (index < prefix.length() + && (Character.isJavaIdentifierPart(prefix.charAt(index)) + || prefix.charAt(index) == '.')) { + index++; + } + while (index < prefix.length() && Character.isWhitespace(prefix.charAt(index))) { + index++; + } + if (index < prefix.length() && prefix.charAt(index) == '(') { + int closing = matchingDelimiter(prefix, index, '(', ')'); + if (closing < 0) { + return false; + } + index = closing + 1; + } + continue; + } + int tokenEnd = index; + while (tokenEnd < prefix.length() && Character.isJavaIdentifierPart(prefix.charAt(tokenEnd))) { + tokenEnd++; + } + if (!modifiers.contains(prefix.substring(index, tokenEnd))) { + return false; + } + index = tokenEnd; + } + return true; + } + + private static Optional> typeVariableBoundsInType( + SourceFile file, TypeInfo type, String variable) { + String source = maskStringLiterals(file.structuralSource()); + int openingBrace = typeBodyOpening(source, type.start()); + if (openingBrace < 0) { + return Optional.empty(); + } + String header = source.substring(type.start(), openingBrace); + Matcher declaration = Pattern.compile( + "\\b" + Pattern.quote(type.kind()) + "\\s+" + Pattern.quote(type.name()) + + "\\s*<") + .matcher(header); + if (!declaration.find()) { + return Optional.empty(); + } + int opening = header.indexOf('<', declaration.end() - 1); + int closing = matchingGenericDelimiter(header, opening); + if (closing < 0) { + return Optional.empty(); + } + return typeVariableBoundsInParameters(header.substring(opening + 1, closing), variable); + } + + private static Optional> typeVariableBoundsInParameters( + String parameters, String variable) { + for (String parameter : splitTopLevelArguments(parameters)) { + String normalizedParameter = stripLeadingTypeParameterAnnotations(parameter); + Matcher declaration = Pattern.compile( + "^\\s*" + Pattern.quote(variable) + "(?:\\s+extends\\s+(.+))?\\s*$") + .matcher(normalizedParameter); + if (!declaration.matches()) { + continue; + } + if (declaration.group(1) == null) { + return Optional.of(List.of()); + } + return Optional.of(splitTypeIntersectionBounds(declaration.group(1)).stream() + .map(SourceMetadataScanner::stripLeadingTypeParameterAnnotations) + .filter(bound -> !bound.isBlank()) + .toList()); + } + return Optional.empty(); + } + + private static String stripLeadingTypeParameterAnnotations(String parameter) { + int index = 0; + while (index < parameter.length()) { + while (index < parameter.length() && Character.isWhitespace(parameter.charAt(index))) { + index++; + } + if (index >= parameter.length() || parameter.charAt(index) != '@') { + break; + } + index++; + while (index < parameter.length() + && (Character.isJavaIdentifierPart(parameter.charAt(index)) + || parameter.charAt(index) == '.')) { + index++; + } + while (index < parameter.length() && Character.isWhitespace(parameter.charAt(index))) { + index++; + } + if (index < parameter.length() && parameter.charAt(index) == '(') { + int closing = matchingDelimiter(parameter, index, '(', ')'); + if (closing < 0) { + return parameter; + } + index = closing + 1; + } + } + return parameter.substring(index).trim(); + } + + private static List splitTypeIntersectionBounds(String expression) { + List result = new ArrayList<>(); + int start = 0; + int parentheses = 0; + int brackets = 0; + int braces = 0; + int angleBrackets = 0; + boolean quoted = false; + boolean escaped = false; + for (int index = 0; index < expression.length(); index++) { + char character = expression.charAt(index); + if (character == '"' && !escaped) { + quoted = !quoted; + } + if (quoted) { + escaped = character == '\\' && !escaped; + if (character != '\\') { + escaped = false; + } + continue; + } + escaped = false; + switch (character) { + case '(' -> parentheses++; + case ')' -> parentheses = Math.max(0, parentheses - 1); + case '[' -> brackets++; + case ']' -> brackets = Math.max(0, brackets - 1); + case '{' -> braces++; + case '}' -> braces = Math.max(0, braces - 1); + case '<' -> { + if (parentheses == 0 && brackets == 0 && braces == 0) { + angleBrackets++; + } + } + case '>' -> { + if (parentheses == 0 && brackets == 0 && braces == 0) { + angleBrackets = Math.max(0, angleBrackets - 1); + } + } + case '&' -> { + if (parentheses == 0 && brackets == 0 && braces == 0 && angleBrackets == 0) { + result.add(expression.substring(start, index).trim()); + start = index + 1; + } + } + default -> { } + } + } + result.add(expression.substring(start).trim()); + return result; + } + + private boolean isJdkTypeOrSubclass( + String candidate, String expectedType, Set visited) { + if (candidate.equals(expectedType)) { + return true; + } + if (!visited.add(candidate) || !expectedType.equals("java.lang.ClassLoader")) { + return false; + } + TypeReference reference = findTypeReference(candidate); + if (reference != null + && resolveParentTypes(reference).stream() + .anyMatch(parent -> isJdkTypeOrSubclass(parent, expectedType, visited))) { + return true; + } + return Optional.ofNullable(KNOWN_CLASSLOADER_SUPERTYPES.get(candidate)) + .map(parent -> isJdkTypeOrSubclass(parent, expectedType, visited)) + .orElse(false); + } + + private static String resolveJdkTypeName(String simpleName) { + return switch (simpleName) { + case "Class" -> "java.lang.Class"; + case "ClassLoader" -> "java.lang.ClassLoader"; + case "Method" -> "java.lang.reflect.Method"; + case "Constructor" -> "java.lang.reflect.Constructor"; + case "InvocationHandler" -> "java.lang.reflect.InvocationHandler"; + case "Proxy" -> "java.lang.reflect.Proxy"; + default -> simpleName; + }; + } + + private static boolean hasOnDemandImport(SourceFile sourceFile, String packageName) { + return Pattern.compile("(?m)^\\s*import\\s+" + Pattern.quote(packageName) + "\\.\\*\\s*;") + .matcher(maskStringLiterals(sourceFile.structuralSource())) + .find(); + } + + /** Reports chained Method.invoke only when the lookup receiver is a JDK Class API. */ + private void addChainedMethodInvokeSites( + List result, + String dynamicSource, + String source, + List receivers) { + addChainedInvocationSites( + result, dynamicSource, source, receivers, + List.of("getDeclaredMethod", "getMethod"), "invoke", "Method.invoke"); + } + + /** Reports chained Constructor.newInstance only when the lookup receiver is a JDK Class API. */ + private void addChainedConstructorSites( + List result, + String dynamicSource, + String source, + List receivers) { + addChainedInvocationSites( + result, dynamicSource, source, receivers, + List.of("getDeclaredConstructor", "getConstructor"), "newInstance", + "Constructor.newInstance"); + } + + private void addChainedInvocationSites( + List result, + String dynamicSource, + String source, + List receivers, + List lookupMethods, + String terminalMethod, + String symbol) { + for (ChainedReceiver receiver : receivers) { + for (String lookupMethod : lookupMethods) { + ChainedLookup lookup = chainedLookup(dynamicSource, receiver, lookupMethod); + if (lookup == null) { + continue; + } + int terminalStart = chainedTerminalStart(dynamicSource, lookup, terminalMethod); + if (terminalStart >= 0) { + addSite(result, source, terminalStart, symbol); + } + } + } + } + + private boolean isJdkClassForNameExpression(SourceFile file, String expression, int offset) { + Matcher owner = CLASS_FOR_NAME_PREFIX.matcher(expression); + if (owner.find()) { + int closing = matchingDelimiter(expression, owner.end() - 1, '(', ')'); + if (closing == expression.length() - 1 + && isJdkApiOwner(file, owner.group(1), offset, "java.lang.Class")) { + return true; + } + } + Matcher thisQualified = THIS_CLASS_FOR_NAME.matcher(expression); + if (thisQualified.find() && thisQualified.start() == 0) { + int closing = matchingDelimiter(expression, thisQualified.end() - 1, '(', ')'); + if (closing == expression.length() - 1 + && hasVisibleField( + file, "Class", thisQualified.group(1), offset + thisQualified.start(1))) { + return true; + } + } + Matcher qualifiedThis = QUALIFIED_THIS_CLASS_FOR_NAME.matcher(expression); + if (!qualifiedThis.find() || qualifiedThis.start() != 0) { + return false; + } + int closing = matchingDelimiter(expression, qualifiedThis.end() - 1, '(', ')'); + if (closing != expression.length() - 1) { + return false; + } + String selectedOwner = qualifiedThisOwner(file, qualifiedThis.group(1), offset + qualifiedThis.start(1)); + return selectedOwner != null + && hasVisibleField( + file, "Class", qualifiedThis.group(2), offset + qualifiedThis.start(2), selectedOwner); + } + + private boolean isJdkClassFieldReceiver(SourceFile file, String receiver, int offset) { + Matcher thisField = THIS_FIELD_RECEIVER.matcher(receiver); + if (thisField.matches()) { + return hasVisibleField(file, "Class", thisField.group(1), offset + thisField.start(1)); + } + Matcher qualifiedThis = QUALIFIED_THIS_FIELD_RECEIVER.matcher(receiver); + if (!qualifiedThis.matches()) { + return false; + } + String selectedOwner = qualifiedThisOwner(file, qualifiedThis.group(1), offset + qualifiedThis.start(1)); + return selectedOwner != null + && hasVisibleField( + file, "Class", qualifiedThis.group(2), offset + qualifiedThis.start(2), selectedOwner); + } + + private void addReceiverCallSites( + List result, + String dynamicSource, + String source, + SourceFile file, + Pattern pattern, + String receiverType) { + Matcher matcher = pattern.matcher(dynamicSource); + while (matcher.find()) { + if (!isBareReceiver(dynamicSource, matcher.start(1))) { + continue; + } + String receiver = matcher.group(1); + boolean apiReceiver = receiver.equals(receiverType) + && isJdkApiOwner(file, receiver, matcher.start(1), receiverType.equals("Class") + ? "java.lang.Class" : "java.lang.ClassLoader"); + if (apiReceiver || isDeclaredVariableBefore(file, receiverType, receiver, matcher.start())) { + addSite(result, source, matcher.start(), matcher.group(2).equals("loadClass") + ? "ClassLoader.loadClass" : matcher.group(2)); + } + } + } + + private void addChainedLookupSites( + List result, + String dynamicSource, + String source, + List receivers, + String... lookupMethods) { + for (ChainedReceiver receiver : receivers) { + // Simple Class receivers are already emitted by addReceiverCallSites. Keep + // them in the shared receiver list for terminal invoke/newInstance chains, + // but do not emit their lookup a second time at a different offset. + if (receiver.kind() == ChainedReceiverKind.SIMPLE + || receiver.kind() == ChainedReceiverKind.THIS_FIELD + || receiver.kind() == ChainedReceiverKind.QUALIFIED_THIS_FIELD) { + continue; + } + for (String lookupMethod : lookupMethods) { + ChainedLookup lookup = chainedLookup(dynamicSource, receiver, lookupMethod); + if (lookup != null) { + addSite(result, source, lookup.methodStart(), lookupMethod); + } + } + } + } + + private static List chainedReceivers(String source) { + List result = new ArrayList<>(); + addChainedForNameReceivers( + result, source, CHAINED_CLASS_FOR_NAME_RECEIVER, ChainedReceiverKind.CLASS_FOR_NAME); + addChainedForNameReceivers( + result, source, THIS_CLASS_FOR_NAME, ChainedReceiverKind.THIS_CLASS_FOR_NAME); + addChainedForNameReceivers( + result, source, QUALIFIED_THIS_CLASS_FOR_NAME, + ChainedReceiverKind.QUALIFIED_THIS_CLASS_FOR_NAME); + addChainedFieldReceivers( + result, source, CHAINED_THIS_FIELD_RECEIVER, ChainedReceiverKind.THIS_FIELD); + addChainedFieldReceivers( + result, source, CHAINED_QUALIFIED_THIS_FIELD_RECEIVER, + ChainedReceiverKind.QUALIFIED_THIS_FIELD); + Matcher getClass = CHAINED_GET_CLASS_RECEIVER.matcher(source); + while (getClass.find()) { + result.add(new ChainedReceiver( + getClass.start(), getClass.end(), getClass.group(), ChainedReceiverKind.GET_CLASS)); + } + Matcher classLiteral = CHAINED_CLASS_LITERAL_RECEIVER.matcher(source); + while (classLiteral.find()) { + result.add(new ChainedReceiver( + classLiteral.start(), classLiteral.end(), classLiteral.group(), + ChainedReceiverKind.CLASS_LITERAL)); + } + Matcher simple = CHAINED_SIMPLE_RECEIVER.matcher(source); + while (simple.find()) { + result.add(new ChainedReceiver( + simple.start(1), simple.end(1), simple.group(1), ChainedReceiverKind.SIMPLE)); + } + return result; + } + + private static void addChainedForNameReceivers( + List result, + String source, + Pattern pattern, + ChainedReceiverKind kind) { + Matcher matcher = pattern.matcher(source); + while (matcher.find()) { + int opening = matcher.end() - 1; + int closing = matchingDelimiter(source, opening, '(', ')'); + if (closing >= 0) { + result.add(new ChainedReceiver( + matcher.start(), closing + 1, + source.substring(matcher.start(), closing + 1), kind)); + } + } + } + + private static void addChainedFieldReceivers( + List result, + String source, + Pattern pattern, + ChainedReceiverKind kind) { + Matcher matcher = pattern.matcher(source); + while (matcher.find()) { + result.add(new ChainedReceiver( + matcher.start(), matcher.end(), source.substring(matcher.start(), matcher.end()), kind)); + } + } + + private boolean isSupportedChainedReceiver( + SourceFile file, String source, ChainedReceiver receiver) { + return switch (receiver.kind()) { + case CLASS_FOR_NAME -> isBareReceiver(source, receiver.start()) + && startsWithJdkClassForName(file, source, receiver.start()); + case THIS_CLASS_FOR_NAME -> isThisClassForNameReceiver(file, source, receiver); + case QUALIFIED_THIS_CLASS_FOR_NAME -> isQualifiedThisClassForNameReceiver( + file, source, receiver); + case THIS_FIELD -> isBareReceiver(source, receiver.start()) + && isJdkClassFieldReceiver(file, receiver.expression(), receiver.start()); + case QUALIFIED_THIS_FIELD -> isJdkClassFieldReceiver( + file, receiver.expression(), receiver.start()); + case GET_CLASS, CLASS_LITERAL -> true; + case SIMPLE -> isBareReceiver(source, receiver.start()) + && isDeclaredVariableBefore(file, "Class", receiver.expression(), receiver.start()); + }; + } + + private boolean isThisClassForNameReceiver( + SourceFile file, String source, ChainedReceiver receiver) { + Matcher matcher = THIS_CLASS_FOR_NAME.matcher(source.substring(receiver.start())); + return isBareReceiver(source, receiver.start()) + && matcher.find() + && matcher.start() == 0 + && hasVisibleField( + file, "Class", matcher.group(1), receiver.start() + matcher.start(1)); + } + + private boolean isQualifiedThisClassForNameReceiver( + SourceFile file, String source, ChainedReceiver receiver) { + Matcher matcher = QUALIFIED_THIS_CLASS_FOR_NAME.matcher(source.substring(receiver.start())); + if (!matcher.find() || matcher.start() != 0) { + return false; + } + String selectedOwner = qualifiedThisOwner( + file, matcher.group(1), receiver.start() + matcher.start(1)); + return selectedOwner != null + && hasVisibleField( + file, "Class", matcher.group(2), receiver.start() + matcher.start(2), selectedOwner); + } + + private static ChainedLookup chainedLookup( + String source, ChainedReceiver receiver, String lookupMethod) { + int dot = skipWhitespace(source, receiver.end()); + if (dot >= source.length() || source.charAt(dot) != '.') { + return null; + } + int methodStart = skipWhitespace(source, dot + 1); + if (!tokenAt(source, methodStart, lookupMethod)) { + return null; + } + int opening = skipWhitespace(source, methodStart + lookupMethod.length()); + if (opening >= source.length() || source.charAt(opening) != '(') { + return null; + } + int closing = matchingDelimiter(source, opening, '(', ')'); + return closing < 0 ? null : new ChainedLookup(methodStart, closing); + } + + private static int chainedTerminalStart( + String source, ChainedLookup lookup, String terminalMethod) { + int dot = skipWhitespace(source, lookup.closing() + 1); + if (dot >= source.length() || source.charAt(dot) != '.') { + return -1; + } + int terminalStart = skipWhitespace(source, dot + 1); + if (!tokenAt(source, terminalStart, terminalMethod)) { + return -1; + } + int opening = skipWhitespace(source, terminalStart + terminalMethod.length()); + return opening < source.length() && source.charAt(opening) == '(' ? terminalStart : -1; + } + + private static boolean tokenAt(String source, int start, String token) { + int end = start + token.length(); + return start >= 0 + && end <= source.length() + && source.startsWith(token, start) + && (end == source.length() || !Character.isJavaIdentifierPart(source.charAt(end))); + } + + private void addThisReceiverSites( + List result, String dynamicSource, String source, SourceFile file) { + Matcher matcher = THIS_RECEIVER_CALL.matcher(dynamicSource); + while (matcher.find()) { + if (isBareReceiver(dynamicSource, matcher.start())) { + addThisReceiverSite( + result, source, file, matcher.group(1), matcher.group(2), + matcher.start(1), null); + } + } + Matcher qualified = QUALIFIED_THIS_RECEIVER_CALL.matcher(dynamicSource); + while (qualified.find()) { + String owner = qualifiedThisOwner(file, qualified.group(1), qualified.start(1)); + if (owner != null) { + addThisReceiverSite( + result, source, file, qualified.group(2), qualified.group(3), + qualified.start(2), owner); + } + } + } + + private void addThisReceiverSite( + List result, + String source, + SourceFile file, + String field, + String method, + int fieldOffset, + String selectedOwner) { + String type = switch (method) { + case "loadClass" -> "ClassLoader"; + case "getDeclaredMethod", "getMethod", "getDeclaredConstructor", "getConstructor" -> "Class"; + case "invoke" -> "Method"; + case "newInstance" -> null; + default -> null; + }; + boolean matches = type != null && hasVisibleField(file, type, field, fieldOffset, selectedOwner); + if (method.equals("newInstance")) { + boolean constructorField = hasVisibleField( + file, "Constructor", field, fieldOffset, selectedOwner); + boolean classField = hasVisibleField(file, "Class", field, fieldOffset, selectedOwner); + matches = constructorField || classField; + type = constructorField ? "Constructor" : classField ? "Class" : null; + } + if (matches) { + addSite(result, source, fieldOffset, dynamicSymbol(method, type)); + } + } + + private static String dynamicSymbol(String method, String type) { + if (method.equals("loadClass")) { + return "ClassLoader.loadClass"; + } + if (method.equals("invoke")) { + return "Method.invoke"; + } + if (method.equals("newInstance")) { + return "Constructor".equals(type) ? "Constructor.newInstance" : "Class.newInstance"; + } + return method; + } + + private void addInlineProxyInvocationSites( + List result, String dynamicSource, String source, SourceFile file) { + for (int proxyOffset : proxyCallOffsets(file, dynamicSource)) { + int opening = dynamicSource.indexOf('(', proxyOffset); + if (opening < 0) { + continue; + } + int closing = matchingDelimiter(dynamicSource, opening, '(', ')'); + if (closing < 0) { + continue; + } + String arguments = dynamicSource.substring(opening + 1, closing); + List callArguments = splitTopLevelArguments(arguments); + if (callArguments.size() < 3) { + continue; + } + String handler = callArguments.get(2); + Matcher lambda = Pattern.compile("\\(([^)]*)\\)\\s*->").matcher(handler); + if (!lambda.find()) { + continue; + } + List parameters = splitTopLevelArguments(lambda.group(1)); + if (parameters.size() != 3) { + continue; + } + String methodParameter = lambdaParameterName(parameters.get(1)); + if (methodParameter.isBlank()) { + continue; + } + int bodyStart = lambda.end(); + while (bodyStart < handler.length() && Character.isWhitespace(handler.charAt(bodyStart))) { + bodyStart++; + } + int bodyEnd = handler.length(); + if (bodyStart < handler.length() && handler.charAt(bodyStart) == '{') { + int closingBody = matchingDelimiter(handler, bodyStart, '{', '}'); + if (closingBody < 0) { + continue; + } + bodyEnd = closingBody + 1; + } + Matcher invoke = Pattern.compile( + "\\b" + Pattern.quote(methodParameter) + "\\s*\\.\\s*invoke\\s*\\(") + .matcher(handler); + invoke.region(lambda.end(), bodyEnd); + int handlerOffset = arguments.indexOf(handler); + while (invoke.find()) { + addSite(result, source, opening + 1 + handlerOffset + invoke.start(), "Method.invoke"); + } + } + } + + private static String lambdaParameterName(String parameter) { + Matcher name = Pattern.compile("([A-Za-z_$][\\w$]*)\\s*$").matcher(parameter.trim()); + return name.find() ? name.group(1) : ""; + } + + private void addReceiverSites( + List result, + String dynamicSource, + String source, + SourceFile file, + String method, + String receiverType, + String symbol) { + Matcher matcher = Pattern.compile( + "\\b([A-Za-z_$][\\w$]*)\\s*\\.\\s*" + method + "\\s*\\(").matcher(dynamicSource); + while (matcher.find()) { + if (isBareReceiver(dynamicSource, matcher.start(1)) + && isDeclaredVariableBefore(file, receiverType, matcher.group(1), matcher.start())) { + addSite(result, source, matcher.start(), symbol); + } + } + } + + private boolean isInvocationHandlerParameterBefore(SourceFile file, String variable, int offset) { + String source = file.structuralSource(); + MethodInfo owner = methodContaining(file, offset); + Pattern lambda = Pattern.compile( + "\\b((?:InvocationHandler|java\\.lang\\.reflect\\.InvocationHandler))\\s+" + + "[A-Za-z_$][\\w$]*\\s*=\\s*\\(([^)]*)\\)\\s*->"); + Matcher matcher = lambda.matcher(source); + matcher.region(owner.name().equals("") ? 0 : owner.start(), + Math.min(offset, source.length())); + while (matcher.find()) { + if (!isJdkDeclaredType(file, matcher.group(1), matcher.start(1), "InvocationHandler")) { + continue; + } + List parameters = splitTopLevelArguments(matcher.group(2)); + if (parameters.size() >= 2 + && lambdaParameterName(parameters.get(1)).equals(variable) + && lambdaBodyContains(source, matcher.end(), offset)) { + return true; + } + } + return false; + } + + private static boolean lambdaBodyContains(String source, int lambdaEnd, int offset) { + int bodyStart = lambdaEnd; + while (bodyStart < source.length() && Character.isWhitespace(source.charAt(bodyStart))) { + bodyStart++; + } + if (bodyStart >= source.length()) { + return false; + } + if (source.charAt(bodyStart) == '{') { + return matchingDelimiter(source, bodyStart, '{', '}') >= offset; + } + int boundary = source.indexOf(';', bodyStart); + return offset >= bodyStart && (boundary < 0 || offset < boundary); + } + + private static void addSite(List result, String source, int offset, String symbol) { + if (result.stream().anyMatch(site -> site.offset() == offset && site.symbol().equals(symbol))) { + return; + } + result.add(new DynamicSite(offset, symbol, dynamicTarget(source, offset, symbol))); + } + + private static boolean isBareReceiver(String source, int receiverStart) { + int previous = receiverStart - 1; + while (previous >= 0 && Character.isWhitespace(source.charAt(previous))) { + previous--; + } + return previous < 0 || source.charAt(previous) != '.'; + } + + private boolean isDeclaredVariableBefore( + SourceFile file, String type, String variable, int offset) { + MethodInfo owner = methodContaining(file, offset); + if (owner.name().equals("")) { + return false; + } + if (hasParameter(owner, variable)) { + return parameterMatchesType(file, owner, type, variable, offset); + } + Optional local = activeLocalDeclaration(file, owner, variable, offset); + if (local.isPresent()) { + LocalDeclaration declaration = local.get(); + if (declaration.type().equals("var")) { + return Optional.ofNullable(inferredReflectionType( + file, declaration.initializer(), declaration.initializerOffset())) + .map(typeName -> typeName.equals(type)) + .orElse(false); + } + return isJdkDeclaredType(file, declaration.type(), declaration.start(), type); + } + return hasVisibleField(file, type, variable, offset); + } + + private String inferredReflectionType(SourceFile file, String initializer, int offset) { + String expression = stripOuterParentheses(initializer.trim()); + if (isRecognizedClassLookup(file, expression, offset) + && Pattern.compile("(?:getDeclaredMethod|getMethod)\\s*\\([^;{}]*\\)$") + .matcher(expression).find()) { + return "Method"; + } + if (isRecognizedClassLookup(file, expression, offset) + && Pattern.compile("(?:getDeclaredConstructor|getConstructor)\\s*\\([^;{}]*\\)$") + .matcher(expression).find()) { + return "Constructor"; + } + if (isStandaloneClassForNameExpression(file, expression, offset)) { + return "Class"; + } + if (expression.matches("^[A-Za-z_$][\\w$.]*\\s*\\.\\s*class$")) { + return "Class"; + } + if (isClassLoaderResultExpression(file, expression, offset)) { + return "ClassLoader"; + } + if (Pattern.compile("^(?:getClass\\s*\\(\\s*\\)|[A-Za-z_$][\\w$]*\\s*\\.\\s*" + + "getClass\\s*\\(\\s*\\))$") + .matcher(expression).find()) { + return "Class"; + } + if (isStandaloneLoadClassExpression(file, expression, offset)) { + return "Class"; + } + return null; + } + + private boolean isStandaloneClassForNameExpression( + SourceFile file, String expression, int offset) { + return isJdkClassForNameExpression(file, expression, offset); + } + + private boolean isClassLoaderResultExpression( + SourceFile file, String expression, int offset) { + Matcher loader = Pattern.compile("^(.+)\\s*\\.\\s*getClassLoader\\s*\\(\\s*\\)$") + .matcher(expression); + if (!loader.find()) { + return false; + } + String receiver = stripOuterParentheses(loader.group(1).trim()); + return isStandaloneClassForNameExpression(file, receiver, offset) + || isStandaloneLoadClassExpression(file, receiver, offset) + || receiver.matches("^[A-Za-z_$][\\w$.]*\\s*\\.\\s*class$") + || isDeclaredVariableBefore(file, "Class", receiver, offset) + || receiver.matches("^(?:getClass\\s*\\(\\s*\\)|[A-Za-z_$][\\w$]*\\s*\\.\\s*" + + "getClass\\s*\\(\\s*\\))$"); + } + + private static String stripOuterParentheses(String expression) { + String result = expression; + while (result.startsWith("(") + && matchingDelimiter(result, 0, '(', ')') == result.length() - 1) { + result = result.substring(1, result.length() - 1).trim(); + } + return result; + } + + private boolean isRecognizedClassLoaderLookup(SourceFile file, String initializer, int offset) { + Matcher lookup = Pattern.compile( + "^(ClassLoader|java\\.lang\\.ClassLoader|[A-Za-z_$][\\w$]*)" + + "\\s*\\.\\s*loadClass\\s*\\(") + .matcher(initializer); + if (!lookup.find()) { + return false; + } + String receiver = lookup.group(1); + if (receiver.equals("ClassLoader") || receiver.equals("java.lang.ClassLoader")) { + return isJdkApiOwner(file, receiver, offset + lookup.start(1), "java.lang.ClassLoader"); + } + return isDeclaredVariableBefore(file, "ClassLoader", receiver, offset + lookup.start(1)); + } + + private boolean isStandaloneLoadClassExpression(SourceFile file, String expression, int offset) { + Matcher lookup = Pattern.compile( + "^(ClassLoader|java\\.lang\\.ClassLoader|[A-Za-z_$][\\w$]*)" + + "\\s*\\.\\s*loadClass\\s*\\(") + .matcher(expression); + if (!lookup.find()) { + return false; + } + int closing = matchingDelimiter(expression, lookup.end() - 1, '(', ')'); + return closing == expression.length() - 1 + && isRecognizedClassLoaderLookup(file, expression, offset); + } + + private boolean isRecognizedClassLookup(SourceFile file, String initializer, int offset) { + Matcher lookup = Pattern.compile( + "^((?:[A-Za-z_$][\\w$]*|java\\.lang\\.Class)\\s*\\.\\s*forName\\s*\\([^;{}]*\\)" + + "|(?:this\\s*\\.\\s*[A-Za-z_$][\\w$]*" + + "|[A-Za-z_$][\\w$.]*\\s*\\.\\s*this\\s*\\.\\s*[A-Za-z_$][\\w$]*)" + + "\\s*\\.\\s*forName\\s*\\([^;{}]*\\)" + + "|(?:this\\s*\\.\\s*[A-Za-z_$][\\w$]*" + + "|[A-Za-z_$][\\w$.]*\\s*\\.\\s*this\\s*\\.\\s*[A-Za-z_$][\\w$]*)" + + "|getClass\\s*\\(\\s*\\)" + + "|[A-Za-z_$][\\w$]*\\s*\\.\\s*getClass\\s*\\(\\s*\\)" + + "|[A-Za-z_$][\\w$]*\\.class" + + "|[A-Za-z_$][\\w$]*)\\s*\\.\\s*" + + "(?:getDeclaredMethod|getMethod|getDeclaredConstructor|getConstructor)" + + "\\s*\\([^;{}]*\\)$") + .matcher(initializer); + if (!lookup.find()) { + return false; + } + String receiver = lookup.group(1).trim(); + String normalizedReceiver = receiver.replaceAll("\\s+", ""); + return normalizedReceiver.equals("getClass()") + || normalizedReceiver.endsWith(".getClass()") + || normalizedReceiver.endsWith(".class") + || isJdkClassForNameExpression(file, receiver, offset + lookup.start(1)) + || isJdkClassFieldReceiver(file, receiver, offset + lookup.start(1)) + || isDeclaredVariableBefore(file, "Class", receiver, offset + lookup.start(1)); + } + + private static boolean hasParameter(MethodInfo owner, String variable) { + return parameters(owner.parameters()).stream().anyMatch(parameter -> parameter.name().equals(variable)); + } + + private boolean parameterMatchesType( + SourceFile file, MethodInfo owner, String type, String variable, int offset) { + return parameters(owner.parameters()).stream() + .filter(parameter -> parameter.name().equals(variable)) + .map(Parameter::type) + .map(parameterType -> stripBalancedGenericArguments(parameterType).trim()) + .anyMatch(parameterType -> isJdkDeclaredType(file, parameterType, offset, type)); + } + + private Optional activeLocalDeclaration( + SourceFile file, MethodInfo owner, String variable, int offset) { + String source = maskStringLiterals(file.structuralSource()); + Pattern declaration = Pattern.compile( + "\\b([A-Za-z_$][\\w$.]*(?:\\s*<[^;=(){}]+>)?(?:\\s*\\[\\])*)\\s+" + + Pattern.quote(variable) + + "\\s*(?:=\\s*([^;{}]+))?(?=\\s*[;,:)])"); + Matcher matcher = declaration.matcher(source); + matcher.region(owner.start(), Math.min(offset, source.length())); + LocalDeclaration selected = null; + while (matcher.find()) { + String declaredType = matcher.group(1).trim(); + if (isDeclarationKeyword(declaredType)) { + continue; + } + if (declarationIsActive(source, matcher.start(1), offset, owner.start())) { + selected = new LocalDeclaration( + declaredType, + matcher.group(2) == null ? "" : matcher.group(2).trim(), + matcher.start(1), + matcher.start(2)); + } + } + Pattern sharedDeclaration = Pattern.compile( + "(?m)(?:^|[;{}:])\\s*" + + "(?:(?:final|volatile|transient)\\s+" + + "|@[A-Za-z_][\\w.]*(?:\\s*\\([^;]*\\))?\\s*)*" + + "([A-Za-z_$][\\w$.]*(?:\\s*<[^;=(){}]+>)?(?:\\s*\\[\\])*)\\s+"); + Pattern basicForSharedDeclaration = Pattern.compile( + "(?m)\\bfor\\s*\\(\\s*" + + "(?:(?:final|volatile|transient)\\s+" + + "|@[A-Za-z_][\\w.]*(?:\\s*\\([^;]*\\))?\\s*)*" + + "([A-Za-z_$][\\w$.]*(?:\\s*<[^;=(){}]+>)?(?:\\s*\\[\\])*)\\s+"); + for (Pattern sharedPattern : List.of(sharedDeclaration, basicForSharedDeclaration)) { + Matcher sharedMatcher = sharedPattern.matcher(source); + sharedMatcher.region(owner.start(), Math.min(offset, source.length())); + while (sharedMatcher.find()) { + int declarationEnd = fieldDeclarationEnd(source, sharedMatcher.end(1)); + if (declarationEnd < 0) { + continue; + } + String declarators = source.substring(sharedMatcher.end(1), declarationEnd); + if (splitFieldDeclarators(declarators).size() < 2) { + continue; + } + Optional declaredType = declaredFieldType( + sharedMatcher.group(1), declarators, variable); + int variableOffset = declaratorVariableOffset(declarators, variable); + if (declaredType.isEmpty() || variableOffset < 0) { + continue; + } + int absoluteVariableOffset = sharedMatcher.end(1) + variableOffset; + if (absoluteVariableOffset >= offset) { + continue; + } + if (!isDeclarationKeyword(declaredType.get()) + && declarationIsActive(source, absoluteVariableOffset, offset, owner.start())) { + selected = new LocalDeclaration(declaredType.get(), "", absoluteVariableOffset, -1); + } + } + } + return Optional.ofNullable(selected); + } + + private boolean hasVisibleField(SourceFile file, String type, String variable, int siteOffset) { + return hasVisibleField(file, type, variable, siteOffset, null); + } + + private boolean hasVisibleField( + SourceFile file, String type, String variable, int siteOffset, String selectedOwner) { + FieldReference visibleField = visibleField(file, variable, siteOffset, selectedOwner); + return visibleField != null + && isJdkDeclaredType( + visibleField.file(), visibleField.type(), visibleField.offset(), type); + } + + private FieldReference visibleField(SourceFile file, String variable, int siteOffset) { + return visibleField(file, variable, siteOffset, null); + } + + private FieldReference visibleField( + SourceFile file, String variable, int siteOffset, String selectedOwner) { + Pattern fieldStart = Pattern.compile( + "(?m)(?:^|[;{}])\\s*(?:@[A-Za-z_][\\w.]*(?:\\s*\\([^;]*\\))?\\s*)*" + + "(?:(?:public|protected|private|static|final|transient|volatile|strictfp)\\s+)*" + + "([A-Za-z_$][\\w$.]*(?:\\s*<[^;=(){}]+>)?(?:\\s*\\[\\])*)\\s+"); + String siteType = typeContaining(file, siteOffset); + Map visibleOwners = selectedOwner == null + ? visibleFieldOwners(file, siteType) + : selectedOwnerFieldOwners(selectedOwner); + FieldReference selected = null; + int selectedDistance = Integer.MAX_VALUE; + for (SourceFile candidate : files()) { + String source = maskStringLiterals(candidate.structuralSource()); + Matcher matcher = fieldStart.matcher(source); + while (matcher.find()) { + int declarationEnd = fieldDeclarationEnd(source, matcher.end(1)); + if (declarationEnd < 0 + || !isTypeMemberScope(candidate, source, declarationScopeIndex(source, matcher.start()))) { + continue; + } + int fieldOffset = matcher.start(1); + Optional fieldType = declaredFieldType( + matcher.group(1), source.substring(matcher.end(1), declarationEnd), variable); + if (fieldType.isEmpty()) { + continue; + } + String fieldOwner = typeContaining(candidate, fieldOffset); + String declaration = source.substring(matcher.start(), declarationEnd + 1); + if (!isAccessibleField(candidate, file, fieldOwner, siteType, declaration)) { + continue; + } + if (!methodContaining(candidate, fieldOffset).name().equals("")) { + continue; + } + Integer distance = visibleOwners.get(fieldOwner); + if (distance == null || distance >= selectedDistance) { + continue; + } + selectedDistance = distance; + selected = new FieldReference(fieldType.get(), candidate, fieldOffset); + } + } + return selected; + } + + private Map selectedOwnerFieldOwners(String owner) { + Map result = new LinkedHashMap<>(); + addVisibleOwnerHierarchy(owner, 0, result, new HashSet<>()); + return result; + } + + private static Optional declaredFieldType( + String sharedType, String declarators, String variable) { + for (String raw : splitFieldDeclarators(declarators)) { + String declaration = raw.trim(); + int equals = declaration.indexOf('='); + if (equals >= 0) { + declaration = declaration.substring(0, equals).trim(); + } + Matcher name = Pattern.compile("^([A-Za-z_$][\\w$]*)").matcher(declaration); + if (!name.find() || !name.group(1).equals(variable)) { + continue; + } + String suffix = declaration.substring(name.end()).replaceAll("\\s+", ""); + if (!suffix.matches("(?:\\[\\])*$")) { + return Optional.empty(); + } + return Optional.of(sharedType.trim() + suffix); + } + return Optional.empty(); + } + + private static int declaratorVariableOffset(String declarators, String variable) { + int search = 0; + for (String raw : splitFieldDeclarators(declarators)) { + int rawStart = declarators.indexOf(raw, search); + if (rawStart < 0) { + continue; + } + Matcher name = Pattern.compile("^([A-Za-z_$][\\w$]*)").matcher(raw); + if (name.find() && name.group(1).equals(variable)) { + return rawStart + name.start(1); + } + search = rawStart + raw.length(); + } + return -1; + } + + private static List splitFieldDeclarators(String source) { + List result = new ArrayList<>(); + int start = 0; + int parentheses = 0; + int brackets = 0; + int braces = 0; + int angleBrackets = 0; + boolean quoted = false; + boolean escaped = false; + for (int index = 0; index < source.length(); index++) { + char character = source.charAt(index); + if (character == '"' && !escaped) { + quoted = !quoted; + } + if (quoted) { + escaped = character == '\\' && !escaped; + if (character != '\\') { + escaped = false; + } + continue; + } + escaped = false; + switch (character) { + case '(' -> parentheses++; + case ')' -> parentheses = Math.max(0, parentheses - 1); + case '[' -> brackets++; + case ']' -> brackets = Math.max(0, brackets - 1); + case '{' -> braces++; + case '}' -> braces = Math.max(0, braces - 1); + case '<' -> { + if (looksLikeGenericStart(source, index, angleBrackets)) { + angleBrackets++; + } + } + case '>' -> angleBrackets = Math.max(0, angleBrackets - 1); + case ',' -> { + if (parentheses == 0 && brackets == 0 && braces == 0 && angleBrackets == 0) { + result.add(source.substring(start, index).trim()); + start = index + 1; + } + } + default -> { } + } + } + result.add(source.substring(start).trim()); + return result; + } + + private static boolean looksLikeGenericStart(String source, int opening, int currentDepth) { + if (currentDepth > 0) { + return true; + } + int previous = previousNonWhitespace(source, opening - 1); + if (previous < 0) { + return false; + } + if (source.charAt(previous) == '.' + || source.charAt(previous) == ':' && previous > 0 + && source.charAt(previous - 1) == ':') { + return true; + } + int wordStart = identifierStart(source, previous); + if (wordStart < 0) { + return false; + } + return "new".equals(source.substring(wordStart, previous + 1)) + || "instanceof".equals(source.substring(wordStart, previous + 1)) + || precededByQualifiedTypeKeyword(source, wordStart, "new") + || precededByQualifiedTypeKeyword(source, wordStart, "instanceof") + || castTypeArgument(source, opening, wordStart); + } + + private static boolean precededByQualifiedTypeKeyword( + String source, int typeStart, String keyword) { + int position = previousNonWhitespace(source, typeStart - 1); + while (position >= 0 && source.charAt(position) == '.') { + position = previousNonWhitespace(source, position - 1); + int qualifierStart = identifierStart(source, position); + if (qualifierStart < 0) { + return false; + } + position = previousNonWhitespace(source, qualifierStart - 1); + } + int keywordStart = identifierStart(source, position); + return keywordStart >= 0 + && keyword.equals(source.substring(keywordStart, position + 1)); + } + + private static boolean castTypeArgument(String source, int opening, int typeStart) { + int beforeType = previousNonWhitespace(source, typeStart - 1); + if (beforeType < 0 || source.charAt(beforeType) != '(') { + return false; + } + int closing = matchingGenericDelimiter(source, opening); + return closing >= 0 && previousNonWhitespace(source, closing + 1) >= 0 + && source.charAt(previousNonWhitespace(source, closing + 1)) == ')'; + } + + private static int previousNonWhitespace(String source, int start) { + int position = start; + while (position >= 0 && Character.isWhitespace(source.charAt(position))) { + position--; + } + return position; + } + + private static int identifierStart(String source, int end) { + if (end < 0 || !Character.isJavaIdentifierPart(source.charAt(end))) { + return -1; + } + int position = end; + while (position >= 0 && Character.isJavaIdentifierPart(source.charAt(position))) { + position--; + } + return position + 1; + } + + private static int fieldDeclarationEnd(String source, int start) { + return fieldInitializerEnd(source, start); + } + + private static boolean isAccessibleField( + SourceFile declaringFile, + SourceFile siteFile, + String declaringType, + String siteType, + String declaration) { + boolean lexicalAccess = siteType.equals(declaringType) + || siteType.startsWith(declaringType + "$"); + if (Pattern.compile("\\bprivate\\b").matcher(declaration).find()) { + return lexicalAccess; + } + if (isInterfaceType(declaringFile, declaringType)) { + return true; + } + return declaringFile.packageName().equals(siteFile.packageName()) + || Pattern.compile("\\b(?:public|protected)\\b").matcher(declaration).find(); + } + + private static int declarationScopeIndex(String source, int matchStart) { + return matchStart < source.length() && ";{}:".indexOf(source.charAt(matchStart)) >= 0 + ? matchStart + 1 + : matchStart; + } + + /** Returns lexical and inherited owners visible from one dynamic call site. */ + private Map visibleFieldOwners(SourceFile file, String siteType) { + Map result = new LinkedHashMap<>(); + String lexical = siteType; + int distance = 0; + Set visited = new HashSet<>(); + while (!lexical.isBlank()) { + distance = addVisibleOwnerHierarchy(lexical, distance, result, visited) + 1; + int separator = lexical.lastIndexOf('$'); + if (separator < 0) { + break; + } + lexical = lexical.substring(0, separator); + } + return result; + } + + private int addVisibleOwnerHierarchy( + String owner, int distance, Map result, Set visited) { + if (owner.isBlank() || !visited.add(owner)) { + return distance; + } + result.putIfAbsent(owner, distance); + TypeReference reference = findTypeReference(owner); + if (reference == null) { + return distance; + } + int deepest = distance; + for (String parent : resolveParentTypes(reference)) { + deepest = Math.max(deepest, addVisibleOwnerHierarchy(parent, distance + 1, result, visited)); + } + return deepest; + } + + private List resolveParentTypes(TypeReference reference) { + String source = maskStringLiterals(reference.file().structuralSource()); + int opening = typeBodyOpening(source, reference.type().start()); + if (opening < 0) { + return List.of(); + } + String header = source.substring(reference.type().start(), opening); + int extendsIndex = topLevelKeywordIndex(header, "extends"); + int implementsIndex = topLevelKeywordIndex(header, "implements"); + int permitsIndex = topLevelKeywordIndex(header, "permits"); + List parentSpellings = new ArrayList<>(); + if (reference.type().kind().equals("interface")) { + if (extendsIndex >= 0) { + int end = permitsIndex >= 0 ? permitsIndex : header.length(); + parentSpellings.addAll(typeNames(header.substring( + extendsIndex + "extends".length(), end))); + } + } else { + if (extendsIndex >= 0) { + int end = implementsIndex >= 0 + ? implementsIndex + : permitsIndex >= 0 ? permitsIndex : header.length(); + parentSpellings.addAll(typeNames(header.substring( + extendsIndex + "extends".length(), end))); + if (!parentSpellings.isEmpty()) { + parentSpellings = new ArrayList<>(parentSpellings.subList(0, 1)); + } + } + if (implementsIndex >= 0) { + int end = permitsIndex >= 0 ? permitsIndex : header.length(); + parentSpellings.addAll(typeNames(header.substring( + implementsIndex + "implements".length(), end))); + } + } + return parentSpellings.stream() + .map(parent -> resolveParentType(parent, reference)) + .filter(Objects::nonNull) + .distinct() + .toList(); + } + + private String resolveParentType(String spelling, TypeReference reference) { + List declared = declaredTypeCandidates( + spelling, reference.file(), reference.type().start()); + if (declared.size() == 1 && declared.get(0).contains("$")) { + return declared.get(0); + } + List imported = importedTypes(spelling, reference.file()); + if (imported.size() > 1) { + return null; + } + if (imported.size() == 1) { + List importedDeclared = declaredTypeCandidates( + imported.get(0), reference.file(), reference.type().start()); + return importedDeclared.size() == 1 + ? importedDeclared.get(0) + : normalizeDeclaredTypeName(imported.get(0), reference.file()); + } + if (declared.size() == 1) { + return declared.get(0); + } + List onDemand = new ArrayList<>(onDemandImportedTypes(spelling, reference.file())); + onDemand.addAll(onDemandImportedJdkTypes(spelling, reference.file())); + onDemand = onDemand.stream().distinct().toList(); + if (onDemand.size() == 1) { + return normalizeDeclaredTypeName(onDemand.get(0), reference.file()); + } + if (onDemand.size() > 1) { + return null; + } + return resolveContractType(spelling, reference.file()).orElse(null); + } + + private TypeReference findTypeReference(String owner) { + for (SourceFile candidate : files()) { + for (TypeInfo type : typeDeclarations(candidate)) { + if (typeContaining(candidate, type.start()).equals(owner)) { + return new TypeReference(candidate, type); + } + } + } + return null; + } + + private String resolveSuperType(TypeReference reference) { + String source = maskStringLiterals(reference.file().structuralSource()); + int opening = typeBodyOpening(source, reference.type().start()); + if (opening < 0) { + return null; + } + Matcher extendsClause = Pattern.compile("\\bextends\\s+([A-Za-z_$][\\w$.]*)") + .matcher(source.substring(reference.type().start(), opening)); + if (!extendsClause.find()) { + return null; + } + String name = extendsClause.group(1); + List imported = importedTypes(name, reference.file()); + if (imported.size() == 1) { + List importedDeclared = declaredTypeCandidates( + imported.get(0), reference.file(), reference.type().start()); + return importedDeclared.size() == 1 + ? importedDeclared.get(0) + : normalizeDeclaredTypeName(imported.get(0), reference.file()); + } + List declared = declaredTypeCandidates(name, reference.file(), reference.type().start()); + return declared.size() == 1 ? declared.get(0) : null; + } + + /** Resolves top-level and nested source types to the binary names used by field lookup. */ + private List declaredTypeCandidates(String spelling, SourceFile sourceFile, int siteOffset) { + String normalized = spelling == null ? "" : spelling.trim(); + if (normalized.isBlank()) { + return List.of(); + } + if (!normalized.contains(".")) { + List local = localTypeCandidates(normalized, sourceFile, siteOffset); + if (!local.isEmpty()) { + return local; + } + } + List declared = files().stream() + .flatMap(candidate -> typeDeclarations(candidate).stream() + .map(type -> typeContaining(candidate, type.start()))) + .distinct() + .toList(); + if (normalized.contains(".")) { + List direct = declared.stream() + .filter(candidate -> candidate.equals(normalized) + || candidate.replace('$', '.').equals(normalized)) + .toList(); + if (!direct.isEmpty()) { + return direct; + } + String qualified = normalized; + if (!qualified.startsWith(sourceFile.packageName() + ".") + && !sourceFile.packageName().isBlank()) { + qualified = sourceFile.packageName() + "." + qualified; + } + final String resolvedQualified = qualified; + final String dotted = resolvedQualified.replace('$', '.'); + return declared.stream() + .filter(candidate -> candidate.equals(resolvedQualified) + || candidate.replace('$', '.').equals(dotted)) + .toList(); + } + + String siteType = typeContaining(sourceFile, siteOffset); + List enclosing = new ArrayList<>(); + String owner = siteType; + while (!owner.isBlank()) { + String nested = owner + "$" + normalized; + if (declared.contains(nested)) { + enclosing.add(nested); + } + int separator = owner.lastIndexOf('$'); + if (separator < 0) { + break; + } + owner = owner.substring(0, separator); + } + if (!enclosing.isEmpty()) { + return List.copyOf(new LinkedHashSet<>(enclosing)); + } + return declared.stream() + .filter(candidate -> candidate.startsWith(sourceFile.packageName() + ".") + || sourceFile.packageName().isBlank() && !candidate.contains(".")) + .filter(candidate -> !candidate.contains("$")) + .filter(candidate -> { + int separator = Math.max(candidate.lastIndexOf('$'), candidate.lastIndexOf('.')); + return candidate.substring(separator + 1).equals(normalized); + }) + .toList(); + } + + /** Resolves member and inherited types in Java's lexical-owner precedence order. */ + private List lexicalTypeCandidates(String simpleName, SourceFile sourceFile, int siteOffset) { + for (String owner : lexicalOwnerNames(sourceFile, siteOffset)) { + List local = localTypeCandidatesForOwner(simpleName, owner, sourceFile, siteOffset); + if (!local.isEmpty()) { + return local; + } + List direct = directMemberTypeCandidates(owner, simpleName); + if (!direct.isEmpty()) { + return direct; + } + List inherited = inheritedMemberTypeCandidates(owner, simpleName, sourceFile); + if (!inherited.isEmpty()) { + return inherited; + } + } + return List.of(); + } + + private List lexicalOwnerNames(SourceFile sourceFile, int siteOffset) { + List containing = typeDeclarations(sourceFile).stream() + .filter(type -> type.start() <= siteOffset && type.end() >= siteOffset) + .sorted(Comparator.comparingInt(TypeInfo::start)) + .toList(); + List result = new ArrayList<>(); + for (int index = containing.size() - 1; index >= 0; index--) { + String owner = typeContaining(sourceFile, containing.get(index).start()); + if (!owner.equals("unknown") && !result.contains(owner)) { + result.add(owner); + } + } + return result; + } + + private String qualifiedThisOwner(SourceFile file, String qualifier, int siteOffset) { + String dottedQualifier = qualifier.replace('$', '.'); + List containing = typeDeclarations(file).stream() + .filter(type -> type.start() <= siteOffset && type.end() >= siteOffset) + .sorted(Comparator.comparingInt(TypeInfo::start).reversed()) + .toList(); + for (TypeInfo type : containing) { + String sourceName = sourceTypeName(file, type); + if (sourceName.equals(dottedQualifier) + || sourceName.endsWith("." + dottedQualifier) + || type.name().equals(qualifier)) { + return typeContaining(file, type.start()); + } + } + return lexicalOwnerNames(file, siteOffset).stream() + .filter(owner -> { + String dottedOwner = owner.replace('$', '.'); + return dottedOwner.equals(dottedQualifier) + || dottedOwner.endsWith("." + dottedQualifier); + }) + .findFirst() + .orElse(null); + } + + private static String sourceTypeName(SourceFile file, TypeInfo target) { + return typeDeclarations(file).stream() + .filter(type -> type.start() <= target.start() && type.end() >= target.start()) + .sorted(Comparator.comparingInt(TypeInfo::start)) + .map(TypeInfo::name) + .collect(java.util.stream.Collectors.joining(".")); + } + + private List localTypeCandidatesForOwner( + String simpleName, String owner, SourceFile sourceFile, int siteOffset) { + TypeReference ownerReference = findTypeReference(owner); + if (ownerReference == null) { + return List.of(); + } + return typeDeclarations(sourceFile).stream() + .filter(type -> type.name().equals(simpleName)) + .filter(type -> isLocalType(sourceFile, type)) + .filter(type -> { + TypeInfo enclosing = enclosingType(sourceFile, type); + return enclosing != null && enclosing.start() == ownerReference.type().start(); + }) + .filter(type -> type.start() <= siteOffset) + .filter(type -> localTypeScopeEnd(sourceFile, type) >= siteOffset) + .sorted(Comparator.comparingInt(TypeInfo::start).reversed()) + .map(type -> typeContaining(sourceFile, type.start())) + .distinct() + .limit(1) + .toList(); + } + + private List directMemberTypeCandidates(String owner, String simpleName) { + String candidate = owner + "$" + simpleName; + return files().stream() + .flatMap(file -> typeDeclarations(file).stream() + .map(type -> typeContaining(file, type.start()))) + .anyMatch(candidate::equals) + ? List.of(candidate) + : List.of(); + } + + private List samePackageTypeCandidates(String requestedName, SourceFile sourceFile) { + return files().stream() + .flatMap(file -> typeDeclarations(file).stream() + .map(type -> typeContaining(file, type.start()))) + .distinct() + .filter(candidate -> !candidate.contains("$")) + .filter(candidate -> sourceFile.packageName().isBlank() + ? !candidate.contains(".") + : candidate.startsWith(sourceFile.packageName() + ".")) + .filter(candidate -> simpleName(candidate).equals(requestedName)) + .toList(); + } + + private List localTypeCandidates(String simpleName, SourceFile sourceFile, int siteOffset) { + return typeDeclarations(sourceFile).stream() + .filter(type -> type.name().equals(simpleName)) + .filter(type -> isLocalType(sourceFile, type)) + .filter(type -> type.start() <= siteOffset) + .filter(type -> localTypeScopeEnd(sourceFile, type) >= siteOffset) + .sorted(Comparator.comparingInt(TypeInfo::start).reversed()) + .map(type -> typeContaining(sourceFile, type.start())) + .distinct() + .limit(1) + .toList(); + } + + private List inheritedMemberTypeCandidates( + String owner, String simpleName, SourceFile siteFile) { + TypeReference reference = findTypeReference(owner); + if (reference == null) { + return List.of(); + } + List frontier = resolveParentTypes(reference); + Set visited = new HashSet<>(); + while (!frontier.isEmpty()) { + List matches = new ArrayList<>(); + List next = new ArrayList<>(); + for (String parent : frontier) { + if (!visited.add(parent)) { + continue; + } + String candidate = parent + "$" + simpleName; + boolean declaresMemberType = directMemberTypeCandidates(parent, simpleName).contains(candidate); + if (declaresMemberType) { + if (isAccessibleMemberType(candidate, siteFile)) { + matches.add(candidate); + } + continue; + } + TypeReference parentReference = findTypeReference(parent); + if (parentReference != null) { + next.addAll(resolveParentTypes(parentReference)); + } + } + if (!matches.isEmpty()) { + return matches.stream().distinct().toList(); + } + frontier = next; + } + return List.of(); + } + + private boolean isAccessibleMemberType(String candidate, SourceFile siteFile) { + TypeReference reference = findTypeReference(candidate); + if (reference == null) { + return false; + } + String source = maskStringLiterals(reference.file().structuralSource()); + int opening = typeBodyOpening(source, reference.type().start()); + if (opening < 0) { + return false; + } + String declaration = memberHeader(source, opening); + if (Pattern.compile("\\bprivate\\b").matcher(declaration).find()) { + return false; + } + TypeInfo enclosing = enclosingType(reference.file(), reference.type()); + if (enclosing != null && enclosing.kind().equals("interface")) { + return true; + } + return reference.file().packageName().equals(siteFile.packageName()) + || Pattern.compile("\\b(?:public|protected)\\b").matcher(declaration).find(); + } + + private static int localTypeScopeEnd(SourceFile sourceFile, TypeInfo type) { + MethodInfo owner = methodContaining(sourceFile, Math.max(0, type.start() - 1)); + if (owner.name().equals("")) { + return type.end(); + } + String source = sourceFile.structuralSource(); + int methodOpening = methodBodyOpening(source, owner.start()); + if (methodOpening < 0) { + return type.end(); + } + int blockOpening = enclosingBlockOpening(source, methodOpening, type.start()); + int blockEnd = blockOpening < 0 ? -1 : matchingDelimiter(source, blockOpening, '{', '}'); + return blockEnd >= 0 ? blockEnd : type.end(); + } + + private static String normalizeDeclaredTypeName(String spelling, SourceFile sourceFile) { + String normalized = spelling == null ? "" : spelling.trim(); + if (normalized.contains("$")) { + return normalized; + } + int packageSeparator = normalized.indexOf('.'); + if (packageSeparator >= 0 && !sourceFile.packageName().isBlank()) { + String packagePrefix = sourceFile.packageName() + "."; + if (normalized.startsWith(packagePrefix)) { + String remainder = normalized.substring(packagePrefix.length()); + return packagePrefix + remainder.replace('.', '$'); + } + } + return normalized; + } + + private static boolean isDeclarationKeyword(String value) { + return Set.of("return", "throw", "new", "if", "for", "while", "switch", "case", "catch", "assert") + .contains(value); + } + + private static boolean declarationIsActive( + String source, int declaration, int offset, int ownerStart) { + DeclarationScope declarationScope = catchOrResourceScope(source, declaration); + if (declarationScope != null) { + return offset >= declarationScope.start() && offset <= declarationScope.end(); + } + ForScope forScope = forInitializerScope(source, declaration); + if (forScope != null) { + if (offset > forScope.end() + || forScope.enhanced() && offset < forScope.bodyStart()) { + return false; + } + } + int methodOpening = methodBodyOpening(source, ownerStart); + if (methodOpening < 0 || offset < methodOpening) { + return false; + } + int opening = enclosingBlockOpening(source, methodOpening, declaration); + return opening >= 0 && matchingDelimiter(source, opening, '{', '}') >= offset; + } + + private static DeclarationScope catchOrResourceScope(String source, int declaration) { + Matcher catchMatcher = Pattern.compile("\\bcatch\\s*\\(").matcher(source); + while (catchMatcher.find()) { + int opening = catchMatcher.end() - 1; + int closing = matchingDelimiter(source, opening, '(', ')'); + if (closing < 0 || declaration <= opening || declaration >= closing) { + continue; + } + int bodyStart = skipWhitespace(source, closing + 1); + if (bodyStart < source.length() && source.charAt(bodyStart) == '{') { + int bodyEnd = matchingDelimiter(source, bodyStart, '{', '}'); + return bodyEnd < 0 ? null : new DeclarationScope(bodyStart, bodyEnd); + } + } + Matcher resourceMatcher = Pattern.compile("\\btry\\s*\\(").matcher(source); + while (resourceMatcher.find()) { + int opening = resourceMatcher.end() - 1; + int closing = matchingDelimiter(source, opening, '(', ')'); + if (closing < 0 || declaration <= opening || declaration >= closing) { + continue; + } + if (!topLevelResourceDeclaration(source, opening, declaration)) { + continue; + } + int bodyStart = skipWhitespace(source, closing + 1); + if (bodyStart < source.length() && source.charAt(bodyStart) == '{') { + int bodyEnd = matchingDelimiter(source, bodyStart, '{', '}'); + return bodyEnd < 0 ? null : new DeclarationScope(declaration, bodyEnd); + } + } + return null; + } + + private static boolean topLevelResourceDeclaration(String source, int opening, int declaration) { + int parentheses = 0; + int brackets = 0; + int braces = 0; + for (int index = opening + 1; index < declaration; index++) { + switch (source.charAt(index)) { + case '(' -> parentheses++; + case ')' -> parentheses = Math.max(0, parentheses - 1); + case '[' -> brackets++; + case ']' -> brackets = Math.max(0, brackets - 1); + case '{' -> braces++; + case '}' -> braces = Math.max(0, braces - 1); + default -> { } + } + } + return parentheses == 0 && brackets == 0 && braces == 0; + } + + private static ForScope forInitializerScope(String source, int declaration) { + Matcher matcher = Pattern.compile("\\bfor\\s*\\(").matcher(source); + ForScope selected = null; + while (matcher.find()) { + int opening = matcher.end() - 1; + int closing = matchingDelimiter(source, opening, '(', ')'); + if (closing < 0 || declaration <= opening || declaration >= closing) { + continue; + } + int semicolon = topLevelHeaderDelimiter(source, opening + 1, closing, ';'); + int colon = semicolon < 0 + ? topLevelHeaderDelimiter(source, opening + 1, closing, ':') + : -1; + boolean enhanced = semicolon < 0 && colon >= 0; + if ((semicolon < 0 || declaration >= semicolon) + && (colon < 0 || declaration >= colon)) { + continue; + } + int bodyStart = closing + 1; + while (bodyStart < source.length() && Character.isWhitespace(source.charAt(bodyStart))) { + bodyStart++; + } + int bodyEnd = bodyStart < source.length() && source.charAt(bodyStart) == '{' + ? matchingDelimiter(source, bodyStart, '{', '}') + : statementEnd(source, bodyStart); + if (bodyEnd >= 0 && (selected == null || opening > selected.opening())) { + selected = new ForScope(opening, bodyStart, bodyEnd, enhanced); + } + } + return selected; + } + + private static int topLevelHeaderDelimiter(String source, int start, int end, char delimiter) { + int parentheses = 0; + int brackets = 0; + int braces = 0; + for (int index = start; index < end; index++) { + switch (source.charAt(index)) { + case '(' -> parentheses++; + case ')' -> parentheses = Math.max(0, parentheses - 1); + case '[' -> brackets++; + case ']' -> brackets = Math.max(0, brackets - 1); + case '{' -> braces++; + case '}' -> braces = Math.max(0, braces - 1); + default -> { } + } + if (source.charAt(index) == delimiter + && parentheses == 0 && brackets == 0 && braces == 0) { + return index; + } + } + return -1; + } + + private static int statementEnd(String source, int start) { + int position = skipWhitespace(source, start); + if (position >= source.length()) { + return source.length() - 1; + } + if (startsKeyword(source, position, "if")) { + return conditionalStatementEnd(source, position, "if"); + } + if (startsKeyword(source, position, "for") + || startsKeyword(source, position, "while") + || startsKeyword(source, position, "synchronized") + || startsKeyword(source, position, "switch")) { + int opening = source.indexOf('(', position); + int closing = opening < 0 ? -1 : matchingDelimiter(source, opening, '(', ')'); + return closing < 0 ? simpleStatementEnd(source, position) : statementEnd(source, closing + 1); + } + if (startsKeyword(source, position, "do")) { + int bodyEnd = statementEnd(source, position + 2); + int whileStart = skipWhitespace(source, bodyEnd + 1); + if (startsKeyword(source, whileStart, "while")) { + int opening = source.indexOf('(', whileStart); + int closing = opening < 0 ? -1 : matchingDelimiter(source, opening, '(', ')'); + return closing < 0 ? simpleStatementEnd(source, whileStart) : statementEnd(source, closing + 1); + } + return bodyEnd; + } + if (startsKeyword(source, position, "try")) { + return tryStatementEnd(source, position); + } + int labelEnd = labelEnd(source, position); + if (labelEnd >= 0) { + return statementEnd(source, labelEnd); + } + if (source.charAt(position) == '{') { + return matchingDelimiter(source, position, '{', '}'); + } + return simpleStatementEnd(source, position); + } + + private static int conditionalStatementEnd(String source, int start, String keyword) { + int opening = source.indexOf('(', start + keyword.length()); + int closing = opening < 0 ? -1 : matchingDelimiter(source, opening, '(', ')'); + if (closing < 0) { + return simpleStatementEnd(source, start); + } + int thenEnd = statementEnd(source, closing + 1); + int elseStart = skipWhitespace(source, thenEnd + 1); + if (startsKeyword(source, elseStart, "else")) { + return statementEnd(source, elseStart + "else".length()); + } + return thenEnd; + } + + private static int tryStatementEnd(String source, int start) { + int bodyStart = skipWhitespace(source, start + "try".length()); + if (bodyStart < source.length() && source.charAt(bodyStart) == '(') { + int resourcesEnd = matchingDelimiter(source, bodyStart, '(', ')'); + if (resourcesEnd < 0) { + return simpleStatementEnd(source, start); + } + bodyStart = skipWhitespace(source, resourcesEnd + 1); + } + int end = statementEnd(source, bodyStart); + int next = skipWhitespace(source, end + 1); + while (startsKeyword(source, next, "catch")) { + int opening = source.indexOf('(', next + "catch".length()); + int closing = opening < 0 ? -1 : matchingDelimiter(source, opening, '(', ')'); + if (closing < 0) { + return end; + } + end = statementEnd(source, closing + 1); + next = skipWhitespace(source, end + 1); + } + if (startsKeyword(source, next, "finally")) { + end = statementEnd(source, next + "finally".length()); + } + return end; + } + + private static int labelEnd(String source, int start) { + int position = start; + if (position >= source.length() || !Character.isJavaIdentifierStart(source.charAt(position))) { + return -1; + } + position++; + while (position < source.length() && Character.isJavaIdentifierPart(source.charAt(position))) { + position++; + } + position = skipWhitespace(source, position); + return position < source.length() && source.charAt(position) == ':' ? position + 1 : -1; + } + + private static int simpleStatementEnd(String source, int start) { + int parentheses = 0; + int brackets = 0; + int braces = 0; + for (int index = start; index < source.length(); index++) { + switch (source.charAt(index)) { + case '(' -> parentheses++; + case ')' -> parentheses = Math.max(0, parentheses - 1); + case '[' -> brackets++; + case ']' -> brackets = Math.max(0, brackets - 1); + case '{' -> braces++; + case '}' -> { + if (braces > 0) { + braces--; + } + } + case ';' -> { + if (parentheses == 0 && brackets == 0 && braces == 0) { + return index; + } + } + default -> { } + } + } + return source.length() - 1; + } + + private static boolean startsKeyword(String source, int start, String keyword) { + return start >= 0 + && start + keyword.length() <= source.length() + && source.startsWith(keyword, start) + && (start == 0 || !Character.isJavaIdentifierPart(source.charAt(start - 1))) + && (start + keyword.length() == source.length() + || !Character.isJavaIdentifierPart(source.charAt(start + keyword.length()))); + } + + private static int methodBodyOpening(String source, int ownerStart) { + int parentheses = 0; + int brackets = 0; + for (int index = ownerStart; index < source.length(); index++) { + switch (source.charAt(index)) { + case '(' -> parentheses++; + case ')' -> parentheses = Math.max(0, parentheses - 1); + case '[' -> brackets++; + case ']' -> brackets = Math.max(0, brackets - 1); + case '{' -> { + if (parentheses == 0 && brackets == 0) { + return index; + } + } + default -> { } + } + } + return -1; + } + + private static int enclosingBlockOpening(String source, int methodOpening, int target) { + List blocks = new ArrayList<>(); + blocks.add(methodOpening); + for (int index = methodOpening + 1; index < Math.min(target, source.length()); index++) { + char character = source.charAt(index); + if (character == '{') { + blocks.add(index); + } else if (character == '}' && blocks.size() > 1) { + blocks.remove(blocks.size() - 1); + } + } + return blocks.get(blocks.size() - 1); + } + + private static String dynamicTarget(String source, int offset, String symbol) { + String literal = firstQuotedArgument(source, offset); + if (symbol.equals("Class.forName") || symbol.equals("ClassLoader.loadClass")) { + return (literal == null ? "unknown" : literal) + "#unknown"; + } + if (symbol.equals("getDeclaredMethod") || symbol.equals("getMethod")) { + return "unknown#" + (literal == null ? "unknown" : literal); + } + return "unknown#unknown"; + } + + private static String firstQuotedArgument(String source, int offset) { + int opening = source.indexOf('(', offset); + if (opening < 0) { + return null; + } + int closing = matchingDelimiter(source, opening, '(', ')'); + if (closing < 0) { + return null; + } + int first = opening + 1; + while (first < closing && Character.isWhitespace(source.charAt(first))) { + first++; + } + if (first >= closing || source.charAt(first) != '"') { + return null; + } + boolean escaped = false; + int end = first + 1; + for (; end < closing; end++) { + char character = source.charAt(end); + if (character == '"' && !escaped) { + break; + } + escaped = character == '\\' && !escaped; + if (character != '\\') { + escaped = false; + } + } + if (end >= closing) { + return null; + } + int remainder = end + 1; + while (remainder < closing && Character.isWhitespace(source.charAt(remainder))) { + remainder++; + } + if (remainder < closing && source.charAt(remainder) != ',') { + return null; + } + return source.substring(first + 1, end); + } + + /** Builds a stable overload discriminator without changing the public callee identity. */ + private static String methodDescriptor(MethodInfo method) { + String parameters = method.parameters().trim(); + if (parameters.isEmpty()) { + return "()"; + } + return "(" + String.join(",", splitTopLevelArguments(parameters).stream() + .map(SourceMetadataScanner::parameterType) + .toList()) + ")"; + } + + private static String parameterType(String parameter) { + String normalized = parameter.trim() + .replaceAll("@[A-Za-z_][\\w.]*", "") + .replace("final ", "") + .trim(); + int nameSeparator = normalized.lastIndexOf(' '); + if (nameSeparator > 0) { + normalized = normalized.substring(0, nameSeparator).trim(); + } + return normalized.replace("...", "[]").replaceAll("\\s+", ""); } /** Builds unresolved evidence for one dynamic proxy construction site. */ - private DynamicFeature dynamicProxyFeature(SourceFile file, MethodInfo caller, String target) { + private DynamicFeature dynamicProxyFeature( + SourceFile file, MethodInfo caller, String target, String targetDescriptor, int offset) { + List symbols = new ArrayList<>(); + symbols.add("Proxy.newProxyInstance"); + if (file.structuralSource().contains("InvocationHandler")) { + symbols.add("InvocationHandler"); + } return new DynamicFeature( DynamicFeature.Kind.DYNAMIC_PROXY, - new MethodSymbol(file.typeName(), caller.name(), ""), + new MethodSymbol(typeContaining(file, offset), caller.name(), ""), target, file.displayPath(), - List.of("Proxy.newProxyInstance", "InvocationHandler"), - "The proxy handler target is not promoted without explicit registration evidence."); + symbols, + "The proxy handler target is not promoted without explicit registration evidence.", + file.lineAt(offset), + offset, + targetDescriptor); } /** Reads source text and derives its package and top-level type identity. */ @@ -583,9 +3226,10 @@ private SourceFile read(Path path, AnalysisContext.SourceRoot root) { try { String source = Files.readString(path); String structuralSource = stripComments(source); - Matcher packageMatcher = PACKAGE.matcher(structuralSource); + String maskedStructuralSource = maskStringLiterals(structuralSource); + Matcher packageMatcher = PACKAGE.matcher(maskedStructuralSource); String packageName = packageMatcher.find() ? packageMatcher.group(1) : ""; - Matcher typeMatcher = TYPE.matcher(structuralSource); + Matcher typeMatcher = TYPE.matcher(maskedStructuralSource); if (!typeMatcher.find()) { throw new IllegalStateException("Java source has no top-level type: " + path); } @@ -612,6 +3256,9 @@ static List methods(SourceFile file) { String maskedSource = maskAnnotationArguments(file.structuralSource()); Matcher matcher = METHOD.matcher(maskedSource); while (matcher.find()) { + if (isControlFlowKeyword(matcher.group(1))) { + continue; + } int start = matcher.start(); String annotationBlock = file.source().substring(start, matcher.start(1)); int annotationOffset = annotationBlock.indexOf('@'); @@ -628,6 +3275,52 @@ static List methods(SourceFile file) { return List.copyOf(result); } + private static boolean isControlFlowKeyword(String name) { + return switch (name) { + case "if", "for", "while", "switch", "catch", "try", "synchronized" -> true; + default -> false; + }; + } + + /** Extracts constructor bodies so dynamic sites retain an explicit initializer caller. */ + private static List constructors(SourceFile file) { + String maskedSource = maskAnnotationArguments(file.structuralSource()); + List result = new ArrayList<>(); + for (TypeInfo type : typeDeclarations(file)) { + Pattern constructor = Pattern.compile( + "(?m)(?:^|[;{}\\n])\\s*" + + "(?:(?:@[A-Za-z_][\\w.]*(?:\\([^)]*\\))?\\s*)|" + + "(?:(?:public|protected|private|static|final|synchronized|strictfp)\\s+))*" + + Pattern.quote(type.name()) + + "\\s*\\(([^)]*)\\)\\s*(?:throws [^{;]+)?\\{"); + Matcher matcher = constructor.matcher(maskedSource); + while (matcher.find()) { + int start = matcher.start(); + String annotationBlock = file.source().substring(start, matcher.start(1)); + int annotationOffset = annotationBlock.indexOf('@'); + int lineOffset = annotationOffset < 0 ? matcher.start(1) : start + annotationOffset; + result.add(new MethodInfo( + "", + file.source().substring(matcher.start(1), matcher.end(1)), + annotationBlock, + start, + matcher.end(), + file.lineAt(lineOffset), + "")); + } + } + return result.stream() + .distinct() + .sorted(Comparator.comparingInt(MethodInfo::start)) + .toList(); + } + + private static List executableMethods(SourceFile file) { + List result = new ArrayList<>(methods(file)); + result.addAll(constructors(file)); + return List.copyOf(result); + } + /** Reports whether a proxy target method name has multiple declared overloads. */ boolean proxyMethodOverloaded(ProxyTarget target) { return methods(target.file()).stream() @@ -663,10 +3356,436 @@ private static String maskAnnotationArguments(String source) { } private static MethodInfo methodContaining(SourceFile file, int index) { - return methods(file).stream() - .filter(method -> method.start() <= index && method.end() >= index) - .findFirst() - .orElseGet(() -> new MethodInfo("", "", "", index, index, file.lineAt(index), "")); + List constructors = constructors(file); + String siteType = typeContaining(file, index); + return executableMethods(file).stream() + .filter(method -> typeContaining(file, method.start()).equals(siteType)) + .filter(method -> constructors.stream().noneMatch(constructor -> + constructor.start() == method.start() && !method.name().equals(""))) + .filter(method -> method.start() <= index) + .filter(method -> method.end() > 0 + && method.end() <= file.structuralSource().length() + && file.structuralSource().charAt(method.end() - 1) == '{') + .filter(method -> matchingDelimiter( + file.structuralSource(), method.end() - 1, '{', '}') >= index) + .max(Comparator.comparingInt(MethodInfo::start)) + .orElseGet(() -> initializerContaining(file, index) + .orElseGet(() -> new MethodInfo( + "", "", "", index, index, file.lineAt(index), ""))); + } + + /** Attributes a dynamic site in a field initializer to the instance or static initializer. */ + private static Optional initializerContaining(SourceFile file, int index) { + String source = maskStringLiterals(file.structuralSource()); + Pattern field = Pattern.compile( + "(?m)(?:^|[;{}])\\s*" + + "(?:(?:@[A-Za-z_][\\w.]*(?:\\([^;{}]*\\))?\\s*)|" + + "(?:(?:public|protected|private|static|final|transient|volatile|strictfp)\\s+))*" + + "[A-Za-z_$][\\w$.<>?,\\[\\] ]*\\s+[A-Za-z_$][\\w$]*\\s*="); + Matcher matcher = field.matcher(source); + MethodInfo selected = initializerBlockContaining(file, source, index).orElse(null); + while (matcher.find()) { + int declarationStart = matcher.start(); + while (declarationStart < matcher.end() + && (Character.isWhitespace(source.charAt(declarationStart)) + || ";{}".indexOf(source.charAt(declarationStart)) >= 0)) { + declarationStart++; + } + if (!isTypeMemberScope(file, source, declarationStart)) { + continue; + } + int equals = source.lastIndexOf('=', matcher.end() - 1); + if (equals >= index) { + continue; + } + int end = fieldInitializerEnd(source, equals + 1); + if (end <= index) { + continue; + } + String declaration = source.substring(matcher.start(), equals); + TypeInfo owner = innermostType(file, declarationStart); + String name = owner != null && owner.kind().equals("interface") + || Pattern.compile("\\bstatic\\b").matcher(declaration).find() + ? "" : ""; + selected = new MethodInfo( + name, + "", + declaration, + matcher.start(), + end + 1, + file.lineAt(matcher.start()), + ""); + } + return Optional.ofNullable(selected); + } + + /** Finds a dynamic site inside a type-level instance or static initializer block. */ + private static Optional initializerBlockContaining( + SourceFile file, String source, int index) { + TypeInfo owner = innermostType(file, index); + if (owner == null) { + return Optional.empty(); + } + int typeOpening = typeBodyOpening(source, owner.start()); + if (typeOpening < 0 || typeOpening >= owner.end()) { + return Optional.empty(); + } + int typeBodyDepth = braceDepthAt(source, typeOpening + 1); + MethodInfo selected = null; + for (int opening = typeOpening + 1; opening < index; opening++) { + if (source.charAt(opening) != '{' + || braceDepthAt(source, opening) != typeBodyDepth + || !isInitializerBlockOpening(file, source, opening)) { + continue; + } + int closing = matchingDelimiter(source, opening, '{', '}'); + if (closing < index) { + continue; + } + String header = memberHeader(source, opening); + String name = header.matches("(?s).*\\bstatic\\b.*") + || owner.kind().equals("interface") ? "" : ""; + selected = new MethodInfo( + name, + "", + header, + opening, + closing + 1, + file.lineAt(opening), + ""); + } + return Optional.ofNullable(selected); + } + + private static boolean isInitializerBlockOpening(SourceFile file, String source, int opening) { + for (TypeInfo type : typeDeclarations(file)) { + int typeOpening = typeBodyOpening(source, type.start()); + if (typeOpening == opening) { + return false; + } + } + String header = memberHeader(source, opening); + return header.isBlank() || header.matches("(?s)\\s*static\\s*"); + } + + private static String memberHeader(String source, int opening) { + int start = opening - 1; + while (start >= 0 && ";{}".indexOf(source.charAt(start)) < 0) { + start--; + } + return source.substring(start + 1, opening).trim(); + } + + /** Keeps field-initializer ownership out of method, block, and lambda locals. */ + private static boolean isTypeMemberScope(SourceFile file, String source, int index) { + TypeInfo owner = innermostType(file, index); + if (owner == null) { + return false; + } + int opening = typeBodyOpening(source, owner.start()); + if (opening < 0 || opening >= owner.end()) { + return false; + } + return braceDepthAt(source, index) == braceDepthAt(source, opening + 1); + } + + private static TypeInfo innermostType(SourceFile file, int index) { + return typeDeclarations(file).stream() + .filter(type -> type.start() <= index && type.end() >= index) + .max(Comparator.comparingInt(TypeInfo::start)) + .orElse(null); + } + + private static int braceDepthAt(String source, int index) { + int depth = 0; + for (int position = 0; position < Math.min(index, source.length()); position++) { + if (source.charAt(position) == '{') { + depth++; + } else if (source.charAt(position) == '}') { + depth = Math.max(0, depth - 1); + } + } + return depth; + } + + private static int fieldInitializerEnd(String source, int start) { + int parentheses = 0; + int brackets = 0; + int braces = 0; + for (int index = start; index < source.length(); index++) { + switch (source.charAt(index)) { + case '(' -> parentheses++; + case ')' -> parentheses = Math.max(0, parentheses - 1); + case '[' -> brackets++; + case ']' -> brackets = Math.max(0, brackets - 1); + case '{' -> braces++; + case '}' -> braces = Math.max(0, braces - 1); + case ';' -> { + if (parentheses == 0 && brackets == 0 && braces == 0) { + return index; + } + } + default -> { } + } + } + return -1; + } + + /** Returns the deepest declared type that contains a source offset. */ + private static String typeContaining(SourceFile file, int index) { + if (anonymousTypeContaining(file, index) != null) { + return "unknown"; + } + List containing = typeDeclarations(file).stream() + .filter(type -> type.start() <= index && type.end() >= index) + .sorted(Comparator.comparingInt(TypeInfo::start)) + .toList(); + if (containing.isEmpty()) { + return file.typeName(); + } + StringBuilder result = new StringBuilder(); + if (!file.packageName().isBlank()) { + result.append(file.packageName()).append('.'); + } + result.append(containing.get(0).name()); + for (int position = 1; position < containing.size(); position++) { + TypeInfo type = containing.get(position); + result.append('$'); + if (isLocalType(file, type)) { + result.append(localTypeOrdinal(file, type)); + } + result.append(type.name()); + } + return result.toString(); + } + + private static boolean isLocalType(SourceFile file, TypeInfo type) { + TypeInfo enclosing = enclosingType(file, type); + if (enclosing == null) { + return false; + } + String source = maskStringLiterals(file.structuralSource()); + int opening = typeBodyOpening(source, enclosing.start()); + return opening >= 0 + && opening < enclosing.end() + && braceDepthAt(source, type.start()) > braceDepthAt(source, opening + 1); + } + + private static int localTypeOrdinal(SourceFile file, TypeInfo type) { + TypeInfo enclosing = enclosingType(file, type); + if (enclosing == null) { + return 1; + } + return (int) typeDeclarations(file).stream() + .filter(candidate -> candidate.start() < type.start()) + .filter(candidate -> candidate.name().equals(type.name())) + .filter(candidate -> isLocalType(file, candidate)) + .filter(candidate -> { + TypeInfo candidateEnclosing = enclosingType(file, candidate); + return candidateEnclosing != null + && candidateEnclosing.start() == enclosing.start(); + }) + .count() + 1; + } + + private static TypeInfo enclosingType(SourceFile file, TypeInfo type) { + return typeDeclarations(file).stream() + .filter(candidate -> candidate.start() < type.start() && candidate.end() >= type.start()) + .max(Comparator.comparingInt(TypeInfo::start)) + .orElse(null); + } + + /** Returns an anonymous-class scope without inventing a source-only binary owner. */ + private static AnonymousType anonymousTypeContaining(SourceFile file, int index) { + String source = maskStringLiterals(file.structuralSource()); + Matcher matcher = NEW_EXPRESSION.matcher(source); + AnonymousType selected = null; + while (matcher.find()) { + int constructorOpening = anonymousConstructorOpening(source, matcher.end()); + if (constructorOpening < 0) { + continue; + } + int constructorClosing = matchingDelimiter(source, constructorOpening, '(', ')'); + if (constructorClosing < 0) { + continue; + } + int opening = constructorClosing + 1; + while (opening < source.length() && Character.isWhitespace(source.charAt(opening))) { + opening++; + } + if (opening >= source.length() || source.charAt(opening) != '{') { + continue; + } + int closing = matchingDelimiter(source, opening, '{', '}'); + if (opening <= index + && closing >= index + && (selected == null || opening > selected.start())) { + selected = new AnonymousType(opening, closing); + } + } + return selected; + } + + /** Locates the constructor parenthesis after explicit type arguments and type-use annotations. */ + private static int anonymousConstructorOpening(String source, int start) { + int position = skipWhitespace(source, start); + if (position < source.length() && source.charAt(position) == '<') { + position = matchingGenericDelimiter(source, position) + 1; + if (position == 0) { + return -1; + } + position = skipWhitespace(source, position); + } + while (position < source.length() && source.charAt(position) == '@') { + position++; + position = skipWhitespace(source, position); + position = qualifiedIdentifierEnd(source, position); + if (position < 0) { + return -1; + } + position = skipWhitespace(source, position); + if (position < source.length() && source.charAt(position) == '(') { + int annotationClosing = matchingDelimiter(source, position, '(', ')'); + if (annotationClosing < 0) { + return -1; + } + position = skipWhitespace(source, annotationClosing + 1); + } + } + position = qualifiedIdentifierEnd(source, position); + if (position < 0) { + return -1; + } + position = skipWhitespace(source, position); + if (position < source.length() && source.charAt(position) == '<') { + int genericClosing = matchingGenericDelimiter(source, position); + if (genericClosing < 0) { + return -1; + } + position = skipWhitespace(source, genericClosing + 1); + } + return position < source.length() && source.charAt(position) == '(' ? position : -1; + } + + private static int skipWhitespace(String source, int start) { + int position = start; + while (position < source.length() && Character.isWhitespace(source.charAt(position))) { + position++; + } + return position; + } + + private static int qualifiedIdentifierEnd(String source, int start) { + int position = start; + if (position >= source.length() || !Character.isJavaIdentifierStart(source.charAt(position))) { + return -1; + } + position++; + while (position < source.length() && Character.isJavaIdentifierPart(source.charAt(position))) { + position++; + } + while (true) { + int dot = skipWhitespace(source, position); + if (dot >= source.length() || source.charAt(dot) != '.') { + return position; + } + int segmentStart = skipWhitespace(source, dot + 1); + if (segmentStart >= source.length() + || !Character.isJavaIdentifierStart(source.charAt(segmentStart))) { + return -1; + } + position = segmentStart + 1; + while (position < source.length() && Character.isJavaIdentifierPart(source.charAt(position))) { + position++; + } + } + } + + private static int matchingGenericDelimiter(String source, int opening) { + int depth = 0; + int parentheses = 0; + int brackets = 0; + int braces = 0; + for (int index = opening; index < source.length(); index++) { + char character = source.charAt(index); + switch (character) { + case '(' -> parentheses++; + case ')' -> parentheses = Math.max(0, parentheses - 1); + case '[' -> brackets++; + case ']' -> brackets = Math.max(0, brackets - 1); + case '{' -> braces++; + case '}' -> braces = Math.max(0, braces - 1); + case '<' -> { + if (parentheses == 0 && brackets == 0 && braces == 0) { + depth++; + } + } + case '>' -> { + if (parentheses == 0 && brackets == 0 && braces == 0 && depth > 0) { + depth--; + if (depth == 0) { + return index; + } + } + } + default -> { } + } + } + return -1; + } + + /** Finds a type body brace without mistaking annotation arrays for the body. */ + private static int typeBodyOpening(String source, int start) { + int parentheses = 0; + int brackets = 0; + int braces = 0; + int angleBrackets = 0; + for (int index = Math.max(0, start); index < source.length(); index++) { + switch (source.charAt(index)) { + case '(' -> parentheses++; + case ')' -> parentheses = Math.max(0, parentheses - 1); + case '[' -> brackets++; + case ']' -> brackets = Math.max(0, brackets - 1); + case '<' -> { + if (parentheses == 0 && brackets == 0 && braces == 0) { + angleBrackets++; + } + } + case '>' -> { + if (parentheses == 0 && brackets == 0 && braces == 0) { + angleBrackets = Math.max(0, angleBrackets - 1); + } + } + case '{' -> { + if (parentheses == 0 && brackets == 0 && braces == 0 && angleBrackets == 0) { + return index; + } + braces++; + } + case '}' -> braces = Math.max(0, braces - 1); + default -> { } + } + } + return -1; + } + + /** Finds declared top-level and nested Java types with bounded body ranges. */ + private static List typeDeclarations(SourceFile file) { + String source = maskStringLiterals(file.structuralSource()); + Matcher matcher = TYPE_DECLARATION.matcher(source); + List result = new ArrayList<>(); + while (matcher.find()) { + int opening = typeBodyOpening(source, matcher.end()); + if (opening < 0) { + continue; + } + int closing = matchingDelimiter(source, opening, '{', '}'); + if (closing < 0) { + continue; + } + result.add(new TypeInfo(matcher.group(1), matcher.group(2), matcher.start(), closing)); + } + return List.copyOf(result); } private static List routeAnnotations(String block, AnalysisContext.Framework framework) { @@ -950,6 +4069,37 @@ private static int keywordIndex(String source, String keyword) { return keywordIndexAfter(source, keyword, 0); } + /** Finds a declaration keyword outside generic, parameter, and array delimiters. */ + private static int topLevelKeywordIndex(String source, String keyword) { + int angleDepth = 0; + int parenthesisDepth = 0; + int bracketDepth = 0; + for (int index = 0; index <= source.length() - keyword.length(); index++) { + char character = source.charAt(index); + if (character == '<') { + angleDepth++; + } else if (character == '>') { + angleDepth = Math.max(0, angleDepth - 1); + } else if (character == '(') { + parenthesisDepth++; + } else if (character == ')') { + parenthesisDepth = Math.max(0, parenthesisDepth - 1); + } else if (character == '[') { + bracketDepth++; + } else if (character == ']') { + bracketDepth = Math.max(0, bracketDepth - 1); + } + if (angleDepth == 0 && parenthesisDepth == 0 && bracketDepth == 0 + && source.startsWith(keyword, index) + && (index == 0 || !Character.isJavaIdentifierPart(source.charAt(index - 1))) + && (index + keyword.length() == source.length() + || !Character.isJavaIdentifierPart(source.charAt(index + keyword.length())))) { + return index; + } + } + return -1; + } + /** Finds a word boundary keyword after a declaration-header offset. */ private static int keywordIndexAfter(String source, String keyword, int offset) { Matcher matcher = Pattern.compile("\\b" + Pattern.quote(keyword) + "\\b") @@ -1394,6 +4544,7 @@ private static String stripComments(String source) { boolean lineComment = false; boolean blockComment = false; boolean stringLiteral = false; + boolean textBlock = false; boolean characterLiteral = false; boolean escaped = false; for (int index = 0; index < source.length(); index++) { @@ -1420,6 +4571,21 @@ private static String stripComments(String source) { } continue; } + if (textBlock) { + if (source.startsWith("\"\"\"", index) && !escaped) { + result.append("\"\"\""); + index += 2; + textBlock = false; + continue; + } + result.append(current); + if (current == '\\' && !escaped) { + escaped = true; + } else { + escaped = false; + } + continue; + } if (!stringLiteral && !characterLiteral && current == '/' && next == '/') { result.append(" "); index++; @@ -1432,6 +4598,13 @@ private static String stripComments(String source) { blockComment = true; continue; } + if (!stringLiteral && !characterLiteral && source.startsWith("\"\"\"", index)) { + result.append("\"\"\""); + index += 2; + textBlock = true; + escaped = false; + continue; + } result.append(current); if (current == '\\' && (stringLiteral || characterLiteral) && !escaped) { escaped = true; @@ -1447,6 +4620,69 @@ private static String stripComments(String source) { return result.toString(); } + /** Masks string and character literal contents while preserving source offsets. */ + private static String maskStringLiterals(String source) { + char[] masked = source.toCharArray(); + boolean stringLiteral = false; + boolean textBlock = false; + boolean characterLiteral = false; + boolean escaped = false; + for (int index = 0; index < source.length(); index++) { + char current = source.charAt(index); + if (textBlock) { + if (source.startsWith("\"\"\"", index) && !escaped) { + masked[index] = ' '; + masked[index + 1] = ' '; + masked[index + 2] = ' '; + index += 2; + textBlock = false; + continue; + } + if (current != '\n' && current != '\r') { + masked[index] = ' '; + } + if (current == '\\' && !escaped) { + escaped = true; + } else { + escaped = false; + } + continue; + } + if (!stringLiteral && !characterLiteral && source.startsWith("\"\"\"", index)) { + masked[index] = ' '; + masked[index + 1] = ' '; + masked[index + 2] = ' '; + index += 2; + textBlock = true; + escaped = false; + continue; + } + if (stringLiteral || characterLiteral) { + if ((stringLiteral && current == '"' || characterLiteral && current == '\'') && !escaped) { + stringLiteral = false; + characterLiteral = false; + continue; + } + if (current == '\\' && !escaped) { + escaped = true; + masked[index] = ' '; + continue; + } + if (current != '\n' && current != '\r') { + masked[index] = ' '; + } + escaped = false; + continue; + } + if (current == '"') { + stringLiteral = true; + } else if (current == '\'') { + characterLiteral = true; + } + } + return new String(masked); + } + /** Removes nested generic arguments from a declaration without leaving trailing brackets. */ private static String stripBalancedGenericArguments(String source) { StringBuilder result = new StringBuilder(source.length()); @@ -1468,14 +4704,41 @@ private static String stripBalancedGenericArguments(String source) { private static int matchingDelimiter(String source, int opening, char open, char close) { int depth = 0; boolean quoted = false; + boolean textBlock = false; + boolean characterLiteral = false; boolean escaped = false; for (int index = opening; index < source.length(); index++) { char character = source.charAt(index); - if (character == '"' && !escaped) { + if (textBlock) { + if (source.startsWith("\"\"\"", index) && !escaped) { + index += 2; + textBlock = false; + continue; + } + if (character == '\\' && !escaped) { + escaped = true; + } else { + escaped = false; + } + continue; + } + if (!quoted && !characterLiteral && source.startsWith("\"\"\"", index)) { + index += 2; + textBlock = true; + escaped = false; + continue; + } + if (character == '"' && !escaped && !characterLiteral) { quoted = !quoted; } - if (quoted) { + if (character == '\'' && !escaped && !quoted) { + characterLiteral = !characterLiteral; + } + if (quoted || characterLiteral) { escaped = character == '\\' && !escaped; + if (character != '\\') { + escaped = false; + } continue; } escaped = false; @@ -1535,19 +4798,6 @@ private static String joinPath(String prefix, String suffix) { return joined.length() > 1 && joined.endsWith("/") ? joined.substring(0, joined.length() - 1) : joined; } - /** Finds the first reflection or dynamic-proxy construct in source text. */ - private static int firstDynamicIndex(String source) { - int reflection = source.indexOf("Class.forName"); - int proxy = source.indexOf("Proxy.newProxyInstance"); - if (reflection < 0) { - return proxy < 0 ? 0 : proxy; - } - if (proxy < 0) { - return reflection; - } - return Math.min(reflection, proxy); - } - private static String qualify(String name, String packageName) { return name.contains(".") ? name : (packageName.isBlank() ? name : packageName + "." + name); } @@ -1557,6 +4807,40 @@ private static String simpleName(String value) { return dot < 0 ? value : value.substring(dot + 1); } + private record DynamicSite(int offset, String symbol, String target) {} + + private enum ChainedReceiverKind { + CLASS_FOR_NAME, + THIS_CLASS_FOR_NAME, + QUALIFIED_THIS_CLASS_FOR_NAME, + THIS_FIELD, + QUALIFIED_THIS_FIELD, + GET_CLASS, + CLASS_LITERAL, + SIMPLE + } + + private record ChainedReceiver( + int start, int end, String expression, ChainedReceiverKind kind) {} + + private record ChainedLookup(int methodStart, int closing) {} + + private record TypeInfo(String kind, String name, int start, int end) {} + + private record TypeVariableBounds(List bounds, int declarationOffset) {} + + private record AnonymousType(int start, int end) {} + + private record TypeReference(SourceFile file, TypeInfo type) {} + + private record FieldReference(String type, SourceFile file, int offset) {} + + private record LocalDeclaration(String type, String initializer, int start, int initializerOffset) {} + + private record DeclarationScope(int start, int end) {} + + private record ForScope(int opening, int bodyStart, int end, boolean enhanced) {} + record SourceFile( Path path, String displayPath, From ec1f6bba5449ce61c8780f491760a4383ee913f0 Mon Sep 17 00:00:00 2001 From: Jordi Gil Date: Mon, 9 Nov 2026 10:19:33 -0500 Subject: [PATCH 03/10] test: cover adapter identities and SootUp integration --- .../java/JavaAnalysisAdapterTest.java | 2756 +++++++++++++++++ .../analysis/java/MethodSymbolTest.java | 26 + ...ootUpBaseGraphProviderIntegrationTest.java | 122 + 3 files changed, 2904 insertions(+) create mode 100644 src/test/java/com/projectlightpath/analysis/java/MethodSymbolTest.java create mode 100644 src/test/java/com/projectlightpath/analysis/java/SootUpBaseGraphProviderIntegrationTest.java diff --git a/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java b/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java index 46d331b..c0e827d 100644 --- a/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java +++ b/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java @@ -243,6 +243,172 @@ public interface AccountRepository extends JpaRepository { } assertTrue(facts.frameworkFacts().stream().anyMatch(item -> item.id().equals("spring-boot-to-data-seeder"))); } + /** Keeps fixture IDs stable only when their complete source identity is unique. */ + @Test + void fixtureSpecificFactIdsRequireUnambiguousFullIdentity() throws IOException { + Path quarkusSource = temp.resolve("id-collisions/quarkus/src/main/java"); + Path quarkusGenerated = temp.resolve("id-collisions/quarkus/generated-src/main/java"); + write(quarkusGenerated, "one/GeneratedTransferMapper.java", """ + package one; + public class GeneratedTransferMapper { + public String toResponse(String value) { return value; } + public String toResponse(int value) { return Integer.toString(value); } + } + """); + write(quarkusGenerated, "two/GeneratedTransferMapper.java", """ + package two; + public class GeneratedTransferMapper { + public String toResponse(String value) { return value; } + } + """); + write(quarkusSource, "one/AuthCallback.java", """ + package one; + public interface AuthCallback { + void onAuthenticated(String user); + void onAuthenticated(int user); + } + """); + write(quarkusSource, "two/AuthCallback.java", """ + package two; + public interface AuthCallback { + void onAuthenticated(String user); + } + """); + write(quarkusSource, "one/AuthProxyFactory.java", """ + package one; + import java.lang.reflect.Proxy; + public class AuthProxyFactory { + AuthCallback create() { + return (AuthCallback) Proxy.newProxyInstance( + AuthCallback.class.getClassLoader(), + new Class[] {AuthCallback.class}, + (proxy, method, args) -> null); + } + } + """); + write(quarkusSource, "two/AuthProxyFactory.java", """ + package two; + import java.lang.reflect.Proxy; + public class AuthProxyFactory { + AuthCallback create() { + return (AuthCallback) Proxy.newProxyInstance( + AuthCallback.class.getClassLoader(), + new Class[] {AuthCallback.class}, + (proxy, method, args) -> null); + } + } + """); + + SemanticCallFacts quarkusFacts = new JavaAnalysisAdapter().analyze( + context( + AnalysisContext.Framework.QUARKUS, + quarkusSource, + quarkusGenerated, + List.of(new AnalysisContext.FrameworkMetadata( + "quarkus-generated-source-root", + "generated_source_root", + AnalysisContext.MetadataStatus.OBSERVED, + "generated-src/main/java"))), + AnalysisGraph.empty()); + List generatedFacts = quarkusFacts.frameworkFacts().stream() + .filter(item -> item.relationshipType().equals("generated_implementation")) + .toList(); + assertEquals(3, generatedFacts.size()); + assertEquals(3, generatedFacts.stream().map(SemanticCallFacts.Fact::id).distinct().count()); + assertFalse(generatedFacts.stream().anyMatch(item -> item.id().equals("quarkus-generated-mapper-binding"))); + + List dynamicProxyFacts = quarkusFacts.frameworkFacts().stream() + .filter(item -> item.relationshipType().equals("proxy_dispatch")) + .toList(); + assertEquals(3, dynamicProxyFacts.size()); + assertEquals(3, dynamicProxyFacts.stream().map(SemanticCallFacts.Fact::id).distinct().count()); + assertFalse(dynamicProxyFacts.stream().anyMatch(item -> item.id().equals("quarkus-unbounded-proxy-abstention"))); + + Path springSource = temp.resolve("id-collisions/spring/src/main/java"); + write(springSource, "first/AccountRepository.java", """ + package first; + public interface AccountRepository + extends org.springframework.data.jpa.repository.JpaRepository { } + """); + write(springSource, "second/AccountRepository.java", """ + package second; + public interface AccountRepository + extends org.springframework.data.jpa.repository.JpaRepository { } + """); + write(springSource, "first/SecurityConfig.java", """ + package first; + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + @Configuration + public class SecurityConfig { + @Bean + public Object securityFilterChain(Object http) { return http; } + @Bean + public Object securityFilterChain(String http) { return http; } + } + """); + write(springSource, "second/SecurityConfig.java", """ + package second; + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + @Configuration + public class SecurityConfig { + @Bean + public Object securityFilterChain(Object http) { return http; } + } + """); + write(springSource, "first/DataSeeder.java", """ + package first; + import org.springframework.boot.CommandLineRunner; + import org.springframework.stereotype.Component; + @Component + public class DataSeeder implements CommandLineRunner { + public void run(String... args) { } + } + """); + write(springSource, "second/DataSeeder.java", """ + package second; + import org.springframework.boot.CommandLineRunner; + import org.springframework.stereotype.Component; + @Component + public class DataSeeder implements CommandLineRunner { + public void run(String... args) { } + } + """); + + SemanticCallFacts springFacts = new JavaAnalysisAdapter().analyze( + context( + AnalysisContext.Framework.SPRING_BOOT, + springSource, + null, + List.of( + new AnalysisContext.FrameworkMetadata( + "spring-bean-annotations", + "source_annotations", + AnalysisContext.MetadataStatus.OBSERVED, + "src/main/java"), + new AnalysisContext.FrameworkMetadata( + "spring-data-repository-contract", + "source_interface_metadata", + AnalysisContext.MetadataStatus.OBSERVED, + "src/main/java"))), + AnalysisGraph.empty()); + List repositoryFacts = springFacts.frameworkFacts().stream() + .filter(item -> item.relationshipType().equals("generated_implementation")) + .toList(); + assertEquals(2, repositoryFacts.size()); + assertEquals(2, repositoryFacts.stream().map(SemanticCallFacts.Fact::id).distinct().count()); + assertFalse(repositoryFacts.stream().anyMatch(item -> item.id().equals("spring-data-generated-account-repository"))); + + List lifecycleFacts = springFacts.frameworkFacts().stream() + .filter(item -> item.relationshipType().equals("lifecycle_callback")) + .toList(); + assertEquals(5, lifecycleFacts.size()); + assertEquals(5, lifecycleFacts.stream().map(SemanticCallFacts.Fact::id).distinct().count()); + assertFalse(lifecycleFacts.stream().anyMatch(item -> item.id().equals("spring-bean-factory-to-security-config"))); + assertFalse(lifecycleFacts.stream().anyMatch(item -> item.id().equals("spring-boot-to-data-seeder"))); + } + /** Enumerates Quarkus route paths and rejects method-only roots. */ @Test void quarkusRoutesEnumerateMethodPathsAndKeepMethodOnlyPaths() throws IOException { @@ -2158,6 +2324,2596 @@ void explicitContractImportWinsOverSamePackageType() throws IOException { assertEquals("demo.ImportedService#", injections.get(0).callee()); } + /** Keeps open-world dynamic behavior abstained even when a provider overstates certainty. */ + @Test + void dynamicBehaviorNeverPromotesAnalyzerEdgesToDefinite() throws IOException { + Path sourceRoot = temp.resolve("dynamic-guards/src/test/java"); + write(sourceRoot, "demo/DynamicBehavior.java", """ + package demo; + import java.lang.reflect.Constructor; + import java.lang.reflect.InvocationHandler; + import java.lang.reflect.Method; + import java.lang.reflect.Proxy; + public class DynamicBehavior { + Object reflect(String typeName) throws Exception { + Class type = Class.forName(typeName); + Method method = type.getMethod("run"); + return method.invoke(type); + } + Runnable proxy() { + return (Runnable) Proxy.newProxyInstance( + Runnable.class.getClassLoader(), new Class[]{Runnable.class}, (p, m, a) -> null); + } + } + """); + write(sourceRoot, "demo/Documentation.java", """ + package demo; + public class Documentation { + // Class.forName, Method.invoke, Proxy.newProxyInstance + String example = "getDeclaredMethod and Constructor.newInstance"; + } + """); + AnalysisContext context = context( + AnalysisContext.Framework.QUARKUS, + sourceRoot, + null, + List.of()); + CallEdge overstatedReflection = new CallEdge( + new MethodSymbol("demo.DynamicBehavior", "reflect", "(Ljava/lang/String;)Ljava/lang/Object;"), + new MethodSymbol("demo.DynamicBehavior", "run", "()V"), + 7, + "src/test/java/demo/DynamicBehavior.java", + SemanticCallFacts.Certainty.DEFINITE, + "reflection"); + + SemanticCallFacts facts = new JavaAnalysisAdapter().analyze(context, new AnalysisGraph( + List.of(overstatedReflection), + List.of(), + Set.of(), + "SootUp", + "3.0.1", + 0, + 0)); + + List dynamicFacts = facts.allFacts().stream() + .filter(item -> item.relationshipType().equals("reflection") + || item.relationshipType().equals("proxy_dispatch")) + .toList(); + assertTrue(dynamicFacts.stream().anyMatch(item -> item.relationshipType().equals("reflection"))); + assertTrue(dynamicFacts.stream().anyMatch(item -> item.relationshipType().equals("proxy_dispatch"))); + assertTrue(dynamicFacts.stream().allMatch(item -> + item.certainty() == SemanticCallFacts.Certainty.UNRESOLVED + && item.status() == SemanticCallFacts.Status.ABSTAINED)); + assertFalse(dynamicFacts.stream().anyMatch(item -> + item.certainty() == SemanticCallFacts.Certainty.DEFINITE)); + assertFalse(facts.providerFacts().stream().anyMatch(item -> + item.evidence().stream().anyMatch(evidence -> evidence.path().contains("Documentation.java")))); + } + + /** Reports each dynamic site under its enclosing method without classifying ordinary invoke calls. */ + @Test + void dynamicSitesKeepCallersAndEquivalentReflectionSpellings() throws IOException { + Path sourceRoot = temp.resolve("dynamic-sites/src/main/java"); + write(sourceRoot, "demo/DynamicSites.java", """ + package demo; + import java.lang.reflect.Method; + import java.net.URLClassLoader; + import java.lang.reflect.Proxy; + import static java.lang.Class.forName; + import static java.lang.reflect.Proxy.newProxyInstance; + public class DynamicSites { + static class AnonymousBase { + AnonymousBase(T value) {} + } + private Method method; + private Method firstField, actionField; + private Method initializedFirst = null, initializedAction = method; + private Method comparisonFirst = (1 < 2) ? null : null, comparisonAction = null; + private int firstFlag = 1, secondFlag = 2; + private Method identifierFirst = (firstFlag < secondFlag) ? null : null, identifierAction = null; + private Method genericFirst = this.genericValue(), genericAction = null; + private Method comparisonBothFirst = firstFlag < secondFlag ? null : null, + comparisonBothAction = firstFlag > secondFlag ? null : null; + @SuppressWarnings({"unused", "unchecked"}) + private Method annotatedFieldFirst, annotatedFieldAction; + @CompositeMarker(@NestedValue("field")) + private Method nestedAnnotatedFieldFirst, nestedAnnotatedFieldAction; + @SuppressWarnings({"unused", "unchecked"}) + private Worker annotatedWorkerFieldFirst, annotatedWorkerFieldAction; + private Method lambdaScopeAction; + private Method forScopeAction; + private Worker forScopeWorkerAction; + private Method enhancedForAction; + private Worker enhancedForWorkerAction; + private Method anonymousBranchAction; + private Method switchScopeAction; + private Worker switchScopeWorkerAction; + private Method tryScopeAction; + private Worker tryScopeWorkerAction; + private Worker labeledScopeAction; + private Method catchScopeAction; + private Method resourceScopeAction; + private Worker catchScopeWorkerAction; + private Method resourceInitializerAction; + private Worker resourceInitializerWorkerAction; + private Method laterDeclaratorAction; + private Worker laterDeclaratorWorkerAction; + private Method multiForWorkerAction; + private Worker multiForMethodAction; + private Method fieldMethod; + private Class fieldClass; + private ClassLoader fieldLoader; + private Constructor fieldConstructor; + private InvocationHandler fieldHandler = (proxy, method, args) -> method.invoke(this, args); + private InvocationHandler blockFieldHandler = (proxy, method, args) -> { + return method.invoke(this, args); + }; + private InvocationHandler blockLocalFieldHandler = (proxy, method, args) -> { + Object result = method.invoke(this, args); + return result; + }; + private static InvocationHandler staticBlockFieldHandler = (proxy, method, args) -> { + return method.invoke(null, args); + }; + private static InvocationHandler staticBlockLocalFieldHandler = (proxy, method, args) -> { + Object result = method.invoke(null, args); + return result; + }; + static { + Class.forName("demo.StaticInitializer"); + } + { + Class.forName("demo.InstanceInitializer"); + } + + Object localClassInitializer() { + class Local { + java.lang.reflect.InvocationHandler handler = + (proxy, method, args) -> method.invoke(null, args); + } + return new Local(); + } + + Object anonymousClassInitializer() { + return new Object() { + java.lang.reflect.InvocationHandler handler = + (proxy, method, args) -> method.invoke(null, args); + }; + } + + Object constructionThenBlock(String name) throws Exception { + new Object(); + if (name != null) { + Class.forName(name); + } + return null; + } + + Object anonymousConstructorArgument(String name) throws Exception { + return new AnonymousBase(Class.forName(name)) {}; + } + + Object explicitAnonymousConstructorArgument(String name) throws Exception { + return new AnonymousBase(Class.forName(name)) {}; + } + + Object explicitAnonymousBody(String name) { + return new AnonymousBase(null) { + Object run() throws Exception { + return Class.forName(name); + } + }; + } + + Object annotatedAnonymousConstructorArgument(String name) throws Exception { + return new @TypeUseMarker AnonymousBase(Class.forName(name)) {}; + } + + Object annotatedAnonymousBody(String name) { + return new @TypeUseMarker AnonymousBase(null) { + Object run() throws Exception { + return Class.forName(name); + } + }; + } + + Object spacedQualifiedAnonymousConstructorArgument(String name) throws Exception { + return new DynamicSites . AnonymousBase(Class.forName(name)) {}; + } + + Object spacedQualifiedAnonymousBody(String name) { + return new java . lang . Object() { + Object run() throws Exception { + return Class.forName(name); + } + }; + } + + DynamicSites(String name) throws Exception { + Class.forName(name); + this.method.invoke(null); + } + + Object first(String name) throws Exception { + Class type = Class . forName(name); + ClassLoader loader = getClass().getClassLoader(); + loader . loadClass(name); + Method method = type.getMethod("run"); + return method . invoke(type); + } + Object qualifiedReceivers(String name) throws Exception { + java.lang.Class type = getClass(); + java.lang.ClassLoader loader = getClass().getClassLoader(); + type.getMethod("run"); + loader.loadClass(name); + return null; + } + Object varReceiver() throws Exception { + var method = getClass().getMethod("run"); + return method.invoke(this); + } + Object qualifiedVarReceiver() throws Exception { + var method = this.getClass().getMethod("run"); + return method.invoke(this); + } + Object varLoader(String name) throws Exception { + var loader = Class.forName(name).getClassLoader(); + loader.loadClass(name); + return null; + } + Object urlClassLoader(String name) throws Exception { + URLClassLoader loader = null; + loader.loadClass(name); + return null; + } + Object customClassLoader(String name) throws Exception { + CustomLoader loader = new CustomLoader(); + loader.loadClass(name); + return null; + } + Object boundedParameter(T loader, String name) throws Exception { + loader.loadClass(name); + return null; + } + Object boundedLocal(T loader, String name) throws Exception { + T local = loader; + local.loadClass(name); + return null; + } + public Object publicBounded(T loader, String name) throws Exception { + loader.loadClass(name); + return null; + } + static Object staticBounded(T loader, String name) throws Exception { + loader.loadClass(name); + return null; + } + @Deprecated Object annotatedBounded(T loader, String name) + throws Exception { + loader.loadClass(name); + return null; + } + <@TypeUseMarker T extends @TypeUseMarker ClassLoader> Object annotatedTypeBound( + T loader, String name) throws Exception { + loader.loadClass(name); + return null; + } + Object bitwiseAnnotatedBound( + T loader, String name) throws Exception { + loader.loadClass(name); + return null; + } + Object intersectionBound( + T loader, String name) throws Exception { + loader.loadClass(name); + return null; + } + <@TypeUseMarker T extends Router> Object annotatedUnrelatedBound(T loader) { + return loader.loadClass(); + } + Object unboundedParameter(T loader, String name) throws Exception { + loader.toString(); + return null; + } + Object unrelatedGeneric(T loader) { + return loader.loadClass(); + } + Object localTypeShadow() throws Exception { + { + class Method { + Object invoke(Object value) { return value; } + } + Method local = new Method(); + local.invoke(null); + } + Method method = null; + return method.invoke(null); + } + Object varClassLiteral() throws Exception { + var type = String.class; + return type.getMethod("valueOf", Object.class); + } + Object varClassLoader(String name) throws Exception { + var type = String.class; + var loader = type.getClassLoader(); + loader.loadClass(name); + return null; + } + Object chainedLoader(String name) throws Exception { + var loader = getClass().getClassLoader(); + var next = loader.loadClass(name).getClassLoader(); + next.loadClass(name); + return null; + } + Object spacedVarReceiver() throws Exception { + var method = this . getClass ().getMethod("run"); + return method.invoke(this); + } + Object varUnrelated(Router router) throws Exception { + var type = router.loadClass(); + return type.getMethod("GET"); + } + Object varScope(Service service) throws Exception { + { + var action = getClass().getMethod("run"); + action.invoke(this); + } + { + Service action = service; + return action.invoke(); + } + } + Object wrappedLookup(Wrapper wrapper) throws Exception { + var action = wrapper.wrap(getClass().getMethod("run")); + return action.invoke(); + } + Object wrappedClass(Wrapper wrapper) throws Exception { + var type = wrapper.wrap(getClass()); + return type.getMethod("run"); + } + Object second(String name) throws Exception { + return forName(name); + } + Object chained(String name) throws Exception { + return Class.forName(name).getMethod("run").invoke(null); + } + Object classLiteral() throws Exception { + return String.class.getMethod("valueOf", Object.class); + } + Object laterString(String name) throws Exception { + Class.forName(name); + return "done"; + } + Object characterLiteral(String name) throws Exception { + char brace = '}'; + return Class.forName(name); + } + Object ordinary(Service service) { + return service.invoke(); + } + Object unrelated(Router router) { + return router.getMethod("GET"); + } + Object unrelatedChained(Router router) { + return router.getMethod("GET").invoke(); + } + Object unrelatedConstructor(Factory factory) { + return factory.getConstructor().newInstance(); + } + Object fieldReceivers(String name) throws Exception { + fieldMethod.invoke(null); + fieldClass.getMethod("run"); + fieldLoader.loadClass(name); + fieldConstructor.newInstance(); + return null; + } + Object multiFieldReceivers() throws Exception { + return actionField.invoke(null); + } + Object initializedMultiFieldReceivers() throws Exception { + return initializedAction.invoke(null); + } + Object comparisonMultiFieldReceivers() throws Exception { + return comparisonAction.invoke(null); + } + Object identifierComparisonMultiFieldReceivers() throws Exception { + return identifierAction.invoke(null); + } + Object genericMultiFieldReceivers() throws Exception { + return genericAction.invoke(null); + } + Object comparisonBothMultiFieldReceivers() throws Exception { + return comparisonBothAction.invoke(null); + } + Object multiLocalReceivers() throws Exception { + Method first = null, action = null; + return action.invoke(null); + } + Object identifierComparisonLocal() throws Exception { + int left = 1, right = 2; + Method first = (left < right) ? null : null, action = null; + return action.invoke(null); + } + Object comparisonBothLocal() throws Exception { + int left = 1, right = 2; + Method first = left < right ? null : null, action = left > right ? null : null; + return action.invoke(null); + } + Object annotatedFieldReceivers() throws Exception { + return annotatedFieldAction.invoke(null); + } + Object nestedAnnotatedFieldReceivers() throws Exception { + return nestedAnnotatedFieldAction.invoke(null); + } + Object annotatedWorkerFieldReceivers() { + return annotatedWorkerFieldAction.invoke(); + } + Object priorAnnotatedLocal() throws Exception { + @SuppressWarnings({"unused"}) int unused = 0; + Method action = null; + return action.invoke(null); + } + Object priorAnnotatedWorkerLocal() { + @SuppressWarnings({"unused"}) int unused = 0; + Worker action = null; + return action.invoke(); + } + Object afterLambdaArgument() throws Exception { + consume(() -> {}); + Method action = null; + return action.invoke(null); + } + Object afterNestedLambdaArgument() throws Exception { + consume(() -> consume(() -> {})); + Method action = null; + return action.invoke(null); + } + Object afterLambdaWorkerArgument() { + consume(() -> {}); + Worker action = null; + return action.invoke(); + } + Object nestedLambdaBlockScope() throws Exception { + consume(() -> { + { Worker lambdaScopeAction = null; lambdaScopeAction.invoke(); } + lambdaScopeAction.invoke(null); + }); + return null; + } + Object controlFlowLambdaBlockScope() throws Exception { + consume(() -> { + if (true) { + Worker lambdaScopeAction = null; + lambdaScopeAction.invoke(); + } + lambdaScopeAction.invoke(null); + }); + return null; + } + Object nestedLambdaWorkerShadow() { + consume(() -> { + { Worker workerOnly = null; } + workerOnly.invoke(); + }); + return null; + } + Object bracedForScope() throws Exception { + for (Worker forScopeAction = null; forScopeAction != null; ) { + forScopeAction.invoke(); + } + return forScopeAction.invoke(null); + } + Object unbracedForScope() throws Exception { + for (Worker forScopeAction = null; forScopeAction != null; ) forScopeAction.invoke(); + return forScopeAction.invoke(null); + } + Object methodLocalForScope() throws Exception { + for (Method forScopeWorkerAction = null; forScopeWorkerAction != null; ) { + forScopeWorkerAction.invoke(null); + } + return forScopeWorkerAction.invoke(); + } + Object enhancedForScope() throws Exception { + for (Worker enhancedForAction : workerValues()) enhancedForAction.invoke(); + return enhancedForAction.invoke(null); + } + Object enhancedForMethodScope() throws Exception { + for (Method enhancedForWorkerAction : methodValues()) { + enhancedForWorkerAction.invoke(null); + } + return enhancedForWorkerAction.invoke(); + } + Object enhancedForIterableScope() throws Exception { + for (Worker enhancedForAction : (Worker[]) enhancedForAction.invoke(null)) { + enhancedForAction.invoke(); + } + return enhancedForAction.invoke(null); + } + Object enhancedForMethodIterableScope() throws Exception { + for (Method enhancedForWorkerAction : (Method[]) enhancedForWorkerAction.invoke(null)) { + enhancedForWorkerAction.invoke(null); + } + return enhancedForWorkerAction.invoke(); + } + Object forIfElseScope() throws Exception { + for (Method enhancedForWorkerAction = null; shouldLoop(); ) + if (false) { } + else enhancedForWorkerAction.invoke(null); + return enhancedForWorkerAction.invoke(); + } + Object anonymousBranchScope() throws Exception { + for (Worker anonymousBranchAction = null; shouldLoop(); ) + if (false) new Runnable() { public void run() {} }; + else anonymousBranchAction.invoke(); + return anonymousBranchAction.invoke(null); + } + Object switchForScope() throws Exception { + for (Worker switchScopeAction = null; shouldLoop(); ) + switch (1) { default: switchScopeAction.invoke(); } + return switchScopeAction.invoke(null); + } + Object switchForMethodScope() throws Exception { + for (Method switchScopeWorkerAction = null; shouldLoop(); ) + switch (1) { default: switchScopeWorkerAction.invoke(null); } + return switchScopeWorkerAction.invoke(); + } + Object catchScope() throws Exception { + try { + throw new RuntimeException(); + } catch (RuntimeException catchScopeAction) { + catchScopeAction.getMessage(); + } + return catchScopeAction.invoke(null); + } + Object catchWorkerScope() throws Exception { + try { + throw new RuntimeException(); + } catch (RuntimeException catchScopeWorkerAction) { + catchScopeWorkerAction.getMessage(); + } + return catchScopeWorkerAction.invoke(); + } + Object resourceScope() throws Exception { + try (CloseableWorker resourceScopeAction = null) { + resourceScopeAction.invoke(); + } + return resourceScopeAction.invoke(null); + } + Object resourceInitializerScope() throws Exception { + try (CloseableWorker resource = makeCloseable(() -> { + Worker resourceInitializerAction = null; + resourceInitializerAction.invoke(); + })) { + resourceInitializerAction.invoke(null); + } + return null; + } + Object resourceInitializerMethodScope() throws Exception { + try (CloseableWorker resource = makeCloseable(() -> { + Method resourceInitializerWorkerAction = null; + resourceInitializerWorkerAction.invoke(null); + })) { + resourceInitializerWorkerAction.invoke(); + } + return null; + } + Object laterDeclaratorBefore() throws Exception { + Worker first = (Worker) laterDeclaratorAction.invoke(null), laterDeclaratorAction = null; + laterDeclaratorAction.invoke(); + return null; + } + Object laterDeclaratorAfter() throws Exception { + Method first = (Method) laterDeclaratorWorkerAction.invoke(), laterDeclaratorWorkerAction = null; + laterDeclaratorWorkerAction.invoke(null); + return null; + } + Object basicForMultiWorkerScope() throws Exception { + for (Worker first = null, multiForWorkerAction = null; + shouldLoop(); + multiForWorkerAction.invoke()) { + multiForWorkerAction.invoke(); + } + return multiForWorkerAction.invoke(null); + } + Object basicForMultiMethodScope() throws Exception { + for (Method first = null, multiForMethodAction = null; + shouldLoop(); + multiForMethodAction.invoke(null)) { + multiForMethodAction.invoke(null); + } + return multiForMethodAction.invoke(); + } + Object forTryFinallyScope() throws Exception { + for (Worker tryScopeAction = null; shouldLoop(); ) + try { } finally { tryScopeAction.invoke(); } + return tryScopeAction.invoke(null); + } + Object forTryMethodScope() throws Exception { + for (Method tryScopeWorkerAction = null; shouldLoop(); ) + try { } finally { tryScopeWorkerAction.invoke(null); } + return tryScopeWorkerAction.invoke(); + } + Object forLabeledIfScope() throws Exception { + for (Method labeledScopeAction = null; shouldLoop(); ) + branch: if (false) { } + else labeledScopeAction.invoke(null); + return labeledScopeAction.invoke(); + } + private Worker[] workerValues() { return null; } + private Method[] methodValues() { return null; } + private CloseableWorker makeCloseable(Runnable task) { return null; } + private boolean shouldLoop() { return true; } + Object finalMultiLocalReceivers() throws Exception { + final Method first = null, action = null; + return action.invoke(null); + } + Object finalWorkerMultiLocal() { + final Worker first = null, action = null; + return action.invoke(); + } + Object annotatedArrayMultiLocal() throws Exception { + @SuppressWarnings({"unused", "unchecked"}) + final Method first = null, action = null; + return action.invoke(null); + } + Object annotatedNestedMultiLocal() throws Exception { + @CompositeMarker(@NestedValue("nested")) + final Method first = null, action = null; + return action.invoke(null); + } + Object annotatedArrayWorkerMultiLocal() { + @SuppressWarnings({"unused", "unchecked"}) + final Worker first = null, action = null; + return action.invoke(); + } + Object multiFieldParameter(Worker actionField) { + return actionField.invoke(); + } + private T genericValue() { return null; } + private void consume(Runnable task) {} + Object shadowed(Service method) { + return method.invoke(); + } + InvocationHandler handler() { + InvocationHandler result = (proxy, method, args) -> method.invoke(new Object(), args); + return result; + } + Runnable proxy() { + return (Runnable) Proxy . newProxyInstance( + Runnable.class.getClassLoader(), new Class[]{Runnable.class}, (p, m, a) -> null); + } + Runnable secondProxy() { + return (Runnable) Proxy.newProxyInstance( + Runnable.class.getClassLoader(), new Class[]{Runnable.class}, (p, m, a) -> null); + } + Runnable proxyWithVariable() { + Class[] interfaces = new Class[]{Runnable.class}; + return (Runnable) Proxy.newProxyInstance( + Runnable.class.getClassLoader(), interfaces, (p, m, a) -> null); + } + Runnable inlineInvocationProxy() { + return (Runnable) Proxy.newProxyInstance( + Runnable.class.getClassLoader(), new Class[]{Runnable.class}, + (proxy, method, args) -> method.invoke(this, args)); + } + Runnable inlineShortProxy() { + return (Runnable) Proxy.newProxyInstance( + Runnable.class.getClassLoader(), new Class[]{Runnable.class}, + (p, m, a) -> m.invoke(this, a)); + } + Runnable inlineTypedProxy() { + return (Runnable) Proxy.newProxyInstance( + Runnable.class.getClassLoader(), new Class[]{Runnable.class}, + (Object p, Method m, Object[] a) -> m.invoke(this, a)); + } + Runnable staticProxy() { + return (Runnable) newProxyInstance( + Runnable.class.getClassLoader(), new Class[]{Runnable.class}, (p, m, a) -> null); + } + Object unrelatedProxyName(Helper helper) { + return helper.newProxyInstance(); + } + Runnable overloadedProxy() { + return (Runnable) Proxy.newProxyInstance( + Runnable.class.getClassLoader(), new Class[]{Overloaded.class}, (p, m, a) -> null); + } + static class NestedOwner { + static class Method { + Object invoke(Object value) { return value; } + } + Object ordinaryInvoke() { + Method method = new Method(); + return method.invoke(null); + } + } + } + @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) + @interface TypeUseMarker {} + @interface ArrayMarker { int[] value(); } + @interface ComparisonMarker { boolean value(); } + @interface BitwiseMarker { int value(); } + @interface NestedValue { String value(); } + @interface CompositeMarker { NestedValue value(); } + interface Service { Object invoke(); } + interface CloseableWorker extends AutoCloseable { + Object invoke(); + default void close() {} + } + interface Router { Object getMethod(String value); Object loadClass(); } + interface Factory { Factory getConstructor(); Object newInstance(); } + interface Wrapper { Object wrap(Object value); } + """); + write(sourceRoot, "StringPackageText.java", """ + class StringPackageText { + String text = "package fake;"; + Object load(String name) { return Class.forName(name); } + } + """); + write(sourceRoot, "demo/CustomLoader.java", """ + package demo; + class CustomLoader extends java.lang.ClassLoader { } + """); + write(sourceRoot, "demo/AnnotatedGeneric.java", """ + package demo; + class AnnotatedGeneric<@TypeUseMarker T extends @TypeUseMarker ClassLoader> { + Object run(T loader, String name) throws Exception { + loader.loadClass(name); + return null; + } + } + """); + write(sourceRoot, "demo/AnnotatedArrayGeneric.java", """ + package demo; + class AnnotatedArrayGeneric<@ArrayMarker({1, 2}) T extends @TypeUseMarker ClassLoader> { + Object run(T loader, String name) throws Exception { + loader.loadClass(name); + return null; + } + } + """); + write(sourceRoot, "demo/ComparisonGeneric.java", """ + package demo; + class ComparisonGeneric<@ComparisonMarker(1 < 2) T extends @TypeUseMarker ClassLoader> { + Object run(T loader, String name) throws Exception { + loader.loadClass(name); + return null; + } + } + """); + write(sourceRoot, "demo/GenericBoundShadow.java", """ + package demo; + class GenericBoundShadow { + Object run(T loader, String name) throws Exception { + loader.loadClass(name); + return null; + } + } + """); + write(sourceRoot, "demo/WildcardLoader.java", """ + package demo; + import java.net.*; + class WildcardLoader { + Object load(String name) throws Exception { + URLClassLoader loader = null; + loader.loadClass(name); + return null; + } + } + """); + write(sourceRoot, "demo/WildcardShadowMethod.java", """ + package demo; + import shadow.*; + class WildcardShadowMethod { + Object invoke() { + Method method = new Method(); + return method.invoke(null); + } + } + """); + write(sourceRoot, "wildcard/Method.java", """ + package wildcard; + public class Method { + public Object invoke(Object... arguments) { return null; } + } + """); + write(sourceRoot, "wildcard/UsesSamePackageMethod.java", """ + package wildcard; + import java.lang.reflect.*; + class UsesSamePackageMethod { + Object invoke() { + Method method = new Method(); + return method.invoke(null); + } + } + """); + write(sourceRoot, "inherited/Base.java", """ + package inherited; + class Base { + protected static class Method { + Object invoke(Object value) { return value; } + } + } + """); + write(sourceRoot, "inherited/Child.java", """ + package inherited; + import java.lang.reflect.Method; + class Child extends Base { + Object ordinary() { + Method method = new Method(); + return method.invoke(null); + } + } + """); + write(sourceRoot, "inheritedprivate/Base.java", """ + package inheritedprivate; + class Base { + private static class Method { + Object invoke(Object value) { return value; } + } + } + """); + write(sourceRoot, "inheritedprivate/Child.java", """ + package inheritedprivate; + import java.lang.reflect.Method; + class Child extends Base { + Object reflective() { + Method method = null; + return method.invoke(null); + } + } + """); + write(sourceRoot, "inheritedbase/Base.java", """ + package inheritedbase; + public class Base { + public static class Method { + public Object invoke(Object value) { return value; } + } + } + """); + write(sourceRoot, "inheritedchild/Child.java", """ + package inheritedchild; + import inheritedbase.Base; + import java.lang.reflect.Method; + class Child extends Base { + Object ordinary() { + Method method = new Method(); + return method.invoke(null); + } + } + """); + write(sourceRoot, "lexicalprecedence/Base.java", """ + package lexicalprecedence; + class Base { + protected static class Method extends java.lang.ClassLoader { } + } + """); + write(sourceRoot, "lexicalprecedence/Outer.java", """ + package lexicalprecedence; + import java.lang.reflect.Method; + class Outer { + static class Method { + Object invoke(Object value) { return value; } + } + static class Inner extends Base { + Object load(String name) throws Exception { + Method loader = null; + loader.loadClass(name); + return null; + } + } + } + """); + write(sourceRoot, "hiddenmembers/A.java", """ + package hiddenmembers; + class A { + protected static class Method { + Object invoke(Object value) { return value; } + } + } + """); + write(sourceRoot, "hiddenmembers/B.java", """ + package hiddenmembers; + class B extends A { + protected static class Method extends java.lang.ClassLoader { } + } + """); + write(sourceRoot, "hiddenmembers/C.java", """ + package hiddenmembers; + import java.lang.reflect.Method; + class C extends B { + Object load(String name) throws Exception { + Method loader = null; + loader.loadClass(name); + return null; + } + } + """); + write(sourceRoot, "localprecedence/LocalPrecedence.java", """ + package localprecedence; + class LocalPrecedence { + Object run(String name) throws Exception { + class Method extends java.lang.ClassLoader { } + class Other { + class Method { + Object loadClass(String value) { return value; } + } + Object ordinary(String value) { + Method loader = null; + return loader.loadClass(value); + } + } + return new Other().ordinary(name); + } + } + """); + write(sourceRoot, "privatehide/A.java", """ + package privatehide; + class A { + protected static class Method { + Object invoke(Object value) { return value; } + } + } + """); + write(sourceRoot, "privatehide/B.java", """ + package privatehide; + class B extends A { + private static class Method { } + } + """); + write(sourceRoot, "privatehide/C.java", """ + package privatehide; + import java.lang.reflect.Method; + class C extends B { + Object reflective() { + Method method = null; + return method.invoke(null); + } + } + """); + write(sourceRoot, "wildcard/WildcardUrlSubclass.java", """ + package wildcard; + import java.net.*; + class WildcardUrlSubclass extends URLClassLoader { + WildcardUrlSubclass() { super(new java.net.URL[0]); } + Object load(String name) throws Exception { + WildcardUrlSubclass loader = this; + loader.loadClass(name); + return null; + } + } + """); + write(sourceRoot, "demo/Overloaded.java", """ + package demo; + public interface Overloaded { + void run(String value); + void run(int value); + } + """); + write(sourceRoot, "demo/Helper.java", """ + package demo; + public interface Helper { Object newProxyInstance(); } + """); + write(sourceRoot, "demo/Outer.java", """ + package demo; + import java.lang.reflect.Method; + public class Outer { + private Worker inherited; + static class Inner { + Inner(String name) throws Exception { Class.forName(name); } + void run(String name) throws Exception { Class.forName(name); } + } + static class SiblingA { + private Method action; + Object reflect() throws Exception { return action.invoke(null); } + } + static class SiblingB { + private Worker action; + Object ordinary() { return action.invoke(); } + } + static class InheritedInner extends Base { + Object reflectInherited() throws Exception { return inherited.invoke(null); } + } + } + """); + write(sourceRoot, "demo/Worker.java", """ + package demo; + public interface Worker { Object invoke(); } + """); + write(sourceRoot, "demo/Base.java", """ + package demo; + import java.lang.reflect.Method; + class Base { protected Method inherited; } + """); + write(sourceRoot, "demo/Child.java", """ + package demo; + class Child extends Base { + Object run() throws Exception { return inherited.invoke(null); } + } + """); + write(sourceRoot, "demo/ChildWithShadow.java", """ + package demo; + class ChildWithShadow extends Base { + private Worker inherited; + Object run() { return inherited.invoke(); } + } + """); + write(sourceRoot, "demo/ReverseBase.java", """ + package demo; + class ReverseBase { protected Worker inherited; } + """); + write(sourceRoot, "demo/ReverseOuter.java", """ + package demo; + import java.lang.reflect.Method; + class ReverseOuter { + private Method inherited; + static class Inner extends ReverseBase { + Object ordinary() throws Exception { return inherited.invoke(null); } + } + } + """); + write(sourceRoot, "demo/ShadowedStatic.java", """ + package demo; + import static java.lang.reflect.Proxy.newProxyInstance; + class ShadowedStatic { + Object newProxyInstance(Object loader, Object interfaces, Object handler) { return null; } + Object call() { return newProxyInstance(null, null, null); } + } + """); + write(sourceRoot, "demo/StaticForNameShadow.java", """ + package demo; + import static java.lang.Class.forName; + class StaticForNameShadow { + Object forName(String name) { return name; } + Object local(String name) { return forName(name); } + Object helper(ForNameHelper helper, String name) { return helper.forName(name); } + } + """); + write(sourceRoot, "demo/InheritedDynamicApiUse.java", """ + package demo; + import static java.lang.Class.forName; + import static java.lang.reflect.Proxy.newProxyInstance; + class InheritedDynamicApis { + Object forName(String name) { return name; } + Object newProxyInstance(Object loader, Object interfaces, Object handler) { return null; } + } + class InheritedDynamicApiUse extends InheritedDynamicApis { + Object className(String name) { return forName(name); } + Object proxy() { return newProxyInstance(null, null, null); } + } + """); + write(sourceRoot, "demo/PrivateDynamicApiUse.java", """ + package demo; + import static java.lang.Class.forName; + import static java.lang.reflect.Proxy.newProxyInstance; + class PrivateDynamicApiUse { + private Object forName(String name) { return name; } + private Object newProxyInstance(Object loader, Object interfaces, Object handler) { return null; } + Object className(String name) { return forName(name); } + Object proxy() { return newProxyInstance(null, null, null); } + } + """); + write(sourceRoot, "demo/InheritedDynamicApiContract.java", """ + package demo; + interface InheritedDynamicApiContract { + default Object forName(String name) { return name; } + default Object newProxyInstance(Object loader, Object interfaces, Object handler) { return null; } + } + """); + write(sourceRoot, "demo/InheritedInterfaceDynamicApiUse.java", """ + package demo; + import static java.lang.Class.forName; + import static java.lang.reflect.Proxy.newProxyInstance; + class InheritedInterfaceDynamicApiUse implements InheritedDynamicApiContract { + Object className(String name) { return forName(name); } + Object proxy() { return newProxyInstance(null, null, null); } + } + """); + write(sourceRoot, "demo/GenericInheritedDynamicApiUse.java", """ + package demo; + import static java.lang.Class.forName; + import static java.lang.reflect.Proxy.newProxyInstance; + class GenericInheritedDynamicApiUse extends InheritedDynamicApis + implements InheritedDynamicApiContract { + Object className(String name) { return forName(name); } + Object proxy() { return newProxyInstance(null, null, null); } + } + """); + write(sourceRoot, "library/Base.java", """ + package library; + public class Base { + public Object forName(String name) { return name; } + public Object newProxyInstance(Object loader, Object interfaces, Object handler) { return null; } + } + """); + write(sourceRoot, "demo/ExplicitImportedParentUse.java", """ + package demo; + import library.Base; + import static java.lang.Class.forName; + import static java.lang.reflect.Proxy.newProxyInstance; + class ExplicitImportedParentUse extends Base { + Object className(String name) { return forName(name); } + Object proxy() { return newProxyInstance(null, null, null); } + } + """); + write(sourceRoot, "library/Outer.java", """ + package library; + public class Outer { + public static class Base { + public java.lang.reflect.Method inherited; + public Object forName(String name) { return name; } + public Object newProxyInstance(Object loader, Object interfaces, Object handler) { return null; } + } + public interface Contract { + default Object forName(String name) { return name; } + default Object newProxyInstance(Object loader, Object interfaces, Object handler) { return null; } + } + } + """); + write(sourceRoot, "demo/ImportedNestedParentUse.java", """ + package demo; + import library.Outer.Base; + import static java.lang.Class.forName; + import static java.lang.reflect.Proxy.newProxyInstance; + class ImportedNestedParentUse extends Base { + Object className(String name) { return forName(name); } + Object inheritedMethod() throws Exception { return inherited.invoke(null); } + Object proxy() { return newProxyInstance(null, null, null); } + } + """); + write(sourceRoot, "demo/ImportedNestedInterfaceUse.java", """ + package demo; + import library.Outer.Contract; + import static java.lang.Class.forName; + import static java.lang.reflect.Proxy.newProxyInstance; + class ImportedNestedInterfaceUse implements Contract { + Object className(String name) { return forName(name); } + Object proxy() { return newProxyInstance(null, null, null); } + } + """); + write(sourceRoot, "library/PackagePrivateBase.java", """ + package library; + import demo.Worker; + public class PackagePrivateBase { Worker action; } + """); + write(sourceRoot, "demo/CrossPackageFieldOuter.java", """ + package demo; + import java.lang.reflect.Method; + import library.PackagePrivateBase; + class CrossPackageFieldOuter { + Method action; + class Inner extends PackagePrivateBase { + Object run() { return action.invoke(); } + } + } + """); + write(sourceRoot, "library/PackagePrivateReflectiveBase.java", """ + package library; + import java.lang.reflect.Method; + public class PackagePrivateReflectiveBase { Method action; } + """); + write(sourceRoot, "demo/CrossPackageWorkerOuter.java", """ + package demo; + import library.PackagePrivateReflectiveBase; + class CrossPackageWorkerOuter { + Worker action; + class Inner extends PackagePrivateReflectiveBase { + Object run() { return action.invoke(); } + } + } + """); + write(sourceRoot, "demo/ForNameHelper.java", """ + package demo; + public interface ForNameHelper { Object forName(String name); } + """); + write(sourceRoot, "demo/OrdinaryForName.java", """ + package demo; + class OrdinaryForName { + static Object forName(String name) { return name; } + } + """); + write(sourceRoot, "demo/FakeStaticImportTextBlock.java", + "package demo;\n" + + "import static demo.OrdinaryForName.forName;\n" + + "class FakeStaticImportTextBlock {\n" + + " String fakeImport = \"\"\"\n" + + " import static java.lang.Class.forName;\n" + + " \"\"\";\n" + + " Object call(String name) { return forName(name); }\n" + + "}\n"); + write(sourceRoot, "demo/FakeExplicitTypeImportTextBlock.java", + "package demo;\n" + + "class FakeExplicitTypeImportTextBlock {\n" + + " String fakeImport = \"\"\"\n" + + " import shadow.Class;\n" + + " \"\"\";\n" + + " Object load(String name) { return Class.forName(name); }\n" + + "}\n"); + write(sourceRoot, "demo/FakeWildcardTypeImportTextBlock.java", + "package demo;\n" + + "class FakeWildcardTypeImportTextBlock {\n" + + " String fakeImport = \"\"\"\n" + + " import shadow.*;\n" + + " \"\"\";\n" + + " Object load(String name) { return Class.forName(name); }\n" + + "}\n"); + write(sourceRoot, "demo/FakeProxyImportTextBlock.java", + "package demo;\n" + + "import external.*;\n" + + "class FakeProxyImportTextBlock {\n" + + " String fakeImport = \"\"\"\n" + + " import java.lang.reflect.*;\n" + + " \"\"\";\n" + + " Object call() { return Proxy.newProxyInstance(null, null, null); }\n" + + "}\n"); + write(sourceRoot, "demo/ShadowedTypeNames.java", """ + package demo; + interface OrdinaryClassReceiver { Object forName(String name); } + interface OrdinaryProxyReceiver { + Object newProxyInstance(Object loader, Object interfaces, Object handler); + } + class ShadowedTypeNames { + private OrdinaryClassReceiver Class; + private OrdinaryProxyReceiver Proxy; + Object fieldClass(String name) { return Class.forName(name); } + Object fieldProxy() { return Proxy.newProxyInstance(null, null, null); } + Object parameterClass(OrdinaryClassReceiver Class, String name) { + return Class.forName(name); + } + Object parameterProxy(OrdinaryProxyReceiver Proxy) { + return Proxy.newProxyInstance(null, null, null); + } + Object localClass(String name) { + OrdinaryClassReceiver Class = null; + return Class.forName(name); + } + Object localProxy() { + OrdinaryProxyReceiver Proxy = null; + return Proxy.newProxyInstance(null, null, null); + } + } + """); + write(sourceRoot, "demo/JdkTypedTypeNames.java", """ + package demo; + class JdkTypedTypeNames { + private Class Class; + private java.lang.reflect.Proxy Proxy; + Object fieldClass(String name) { return Class.forName(name); } + Object fieldProxy() { return Proxy.newProxyInstance(null, null, null); } + Object thisFieldClass(String name) { return this.Class.forName(name); } + Object thisFieldProxy() { return this.Proxy.newProxyInstance(null, null, null); } + Object parameterClass(Class Class, String name) { return Class.forName(name); } + Object parameterProxy(java.lang.reflect.Proxy Proxy) { + return Proxy.newProxyInstance(null, null, null); + } + Object aliasClass(Class klass, String name) { return klass.forName(name); } + Object aliasProxy(java.lang.reflect.Proxy proxy) { + return proxy.newProxyInstance(null, null, null); + } + Object localClass(String name) { + Class Class = null; + return Class.forName(name); + } + Object localProxy() { + java.lang.reflect.Proxy Proxy = null; + return Proxy.newProxyInstance(null, null, null); + } + } + """); + write(sourceRoot, "demo/QualifiedShadowedTypeNames.java", """ + package demo; + class QualifiedShadowedTypeNames { + private OrdinaryClassReceiver klass; + private OrdinaryProxyReceiver proxy; + Object classCall(Class klass, String name) { return this.klass.forName(name); } + Object proxyCall(java.lang.reflect.Proxy proxy) { + return this.proxy.newProxyInstance(null, null, null); + } + Object classWhitespace(Class klass, String name) { return this. klass.forName(name); } + Object proxyWhitespace(java.lang.reflect.Proxy proxy) { + return this. proxy.newProxyInstance(null, null, null); + } + Object classComment(Class klass, String name) { + return this. /* ordinary receiver */ klass.forName(name); + } + Object proxyComment(java.lang.reflect.Proxy proxy) { + return this. /* ordinary receiver */ proxy.newProxyInstance(null, null, null); + } + } + """); + write(sourceRoot, "demo/QualifiedThisOwners.java", """ + package demo; + class QualifiedThisOwners { + private OrdinaryClassReceiver classField; + private OrdinaryProxyReceiver proxyField; + class Inner { + private Class classField; + private java.lang.reflect.Proxy proxyField; + Object outerClass(String name) { + return QualifiedThisOwners.this.classField.forName(name); + } + Object outerProxy() { + return QualifiedThisOwners.this.proxyField.newProxyInstance(null, null, null); + } + Object outerClassWhitespace(String name) { + return QualifiedThisOwners. this.classField.forName(name); + } + Object outerProxyWhitespace() { + return QualifiedThisOwners. this.proxyField.newProxyInstance(null, null, null); + } + Object outerClassComment(String name) { + return QualifiedThisOwners. /* ordinary receiver */ this.classField.forName(name); + } + Object outerProxyComment() { + return QualifiedThisOwners. /* ordinary receiver */ this.proxyField + .newProxyInstance(null, null, null); + } + } + } + """); + write(sourceRoot, "demo/QualifiedThisJdkOwners.java", """ + package demo; + class QualifiedThisJdkOwners { + private Class classField; + private java.lang.reflect.Proxy proxyField; + class Inner { + private OrdinaryClassReceiver classField; + private OrdinaryProxyReceiver proxyField; + Object outerClass(String name) { + return QualifiedThisJdkOwners.this.classField.forName(name); + } + Object outerProxy() { + return QualifiedThisJdkOwners.this.proxyField.newProxyInstance(null, null, null); + } + } + } + """); + write(sourceRoot, "demo/QualifiedThisInheritedJdkBase.java", """ + package demo; + class QualifiedThisInheritedJdkBase { + protected Class classField; + protected java.lang.reflect.Proxy proxyField; + } + """); + write(sourceRoot, "demo/QualifiedThisInheritedJdkOwners.java", """ + package demo; + class QualifiedThisInheritedJdkOwners extends QualifiedThisInheritedJdkBase { + class Inner { + Object outerClass(String name) { + return QualifiedThisInheritedJdkOwners.this.classField.forName(name); + } + Object outerProxy() { + return QualifiedThisInheritedJdkOwners.this.proxyField.newProxyInstance( + null, null, null); + } + } + } + """); + write(sourceRoot, "demo/QualifiedThisHiddenOrdinaryBase.java", """ + package demo; + class QualifiedThisHiddenOrdinaryBase { + protected Class classField; + protected java.lang.reflect.Proxy proxyField; + } + """); + write(sourceRoot, "demo/QualifiedThisHiddenOrdinaryOwners.java", """ + package demo; + class QualifiedThisHiddenOrdinaryOwners extends QualifiedThisHiddenOrdinaryBase { + private OrdinaryClassReceiver classField; + private OrdinaryProxyReceiver proxyField; + class Inner { + Object outerClass(String name) { + return QualifiedThisHiddenOrdinaryOwners.this.classField.forName(name); + } + Object outerProxy() { + return QualifiedThisHiddenOrdinaryOwners.this.proxyField.newProxyInstance( + null, null, null); + } + } + } + """); + write(sourceRoot, "demo/LocalQualifiedThisOwners.java", """ + package demo; + class LocalQualifiedThisOwners { + Object run(String name) { + class Local { + private Class klass; + private java.lang.reflect.Proxy proxy; + Object classCall(String value) { return Local.this.klass.forName(value); } + Object proxyCall() { + return Local.this.proxy.newProxyInstance(null, null, null); + } + } + return new Local().classCall(name); + } + } + """); + write(sourceRoot, "demo/QualifiedThisOtherApis.java", """ + package demo; + interface OrdinaryDynamicReceiver { + Object loadClass(String name); + Object getMethod(String name); + Object getDeclaredConstructor(); + Object invoke(Object value); + Object newInstance(); + } + class QualifiedThisOtherOrdinaryOwners { + private OrdinaryDynamicReceiver loader; + private OrdinaryDynamicReceiver type; + private OrdinaryDynamicReceiver method; + private OrdinaryDynamicReceiver constructor; + class Inner { + private ClassLoader loader; + private Class type; + private java.lang.reflect.Method method; + private java.lang.reflect.Constructor constructor; + Object load(String name) throws Exception { + return QualifiedThisOtherOrdinaryOwners.this.loader.loadClass(name); + } + Object lookup() throws Exception { + QualifiedThisOtherOrdinaryOwners.this.type.getMethod("run"); + QualifiedThisOtherOrdinaryOwners.this.type.getDeclaredConstructor(); + return null; + } + Object invoke() throws Exception { + return QualifiedThisOtherOrdinaryOwners.this.method.invoke(null); + } + Object construct() throws Exception { + return QualifiedThisOtherOrdinaryOwners.this.constructor.newInstance(); + } + } + } + class QualifiedThisOtherJdkOwners { + private ClassLoader loader; + private Class type; + private java.lang.reflect.Method method; + private java.lang.reflect.Constructor constructor; + class Inner { + private OrdinaryDynamicReceiver loader; + private OrdinaryDynamicReceiver type; + private OrdinaryDynamicReceiver method; + private OrdinaryDynamicReceiver constructor; + Object load(String name) throws Exception { + return QualifiedThisOtherJdkOwners.this.loader.loadClass(name); + } + Object lookup() throws Exception { + QualifiedThisOtherJdkOwners.this.type.getMethod("run"); + QualifiedThisOtherJdkOwners.this.type.getDeclaredConstructor(); + return null; + } + Object invoke() throws Exception { + return QualifiedThisOtherJdkOwners.this.method.invoke(null); + } + Object construct() throws Exception { + return QualifiedThisOtherJdkOwners.this.constructor.newInstance(); + } + } + } + """); + write(sourceRoot, "demo/ChainedQualifiedShadowNames.java", """ + package demo; + interface OrdinaryClassChain { + OrdinaryLookup forName(String name); + } + interface OrdinaryLookup { + OrdinaryChainMethod getMethod(String name); + OrdinaryChainConstructor getDeclaredConstructor(); + } + interface OrdinaryChainMethod { + Object invoke(Object value); + } + interface OrdinaryChainConstructor { + Object newInstance(); + } + class ChainedQualifiedShadowNames { + private Class classAlias; + private OrdinaryClassChain ordinaryClassAlias; + class Holder { + OrdinaryDynamicReceiver klass; + OrdinaryClassChain Class; + } + Object invoke(Holder holder, Class klass) throws Exception { + return holder.klass.getMethod("run").invoke(null); + } + Object construct(Holder holder, Class klass) throws Exception { + return holder.klass.getDeclaredConstructor().newInstance(); + } + Object lookup(Holder holder, Class Class) throws Exception { + return holder.Class.forName("x").getMethod("run"); + } + Object lookupConstructor(Holder holder, Class Class) throws Exception { + return holder.Class.forName("x").getDeclaredConstructor(); + } + Object realLookup(String name) throws Exception { + return Class.forName(name).getMethod("run"); + } + Object realLookupConstructor(String name) throws Exception { + return Class.forName(name).getDeclaredConstructor(); + } + Object simpleMethodLookup(Class type) throws Exception { + return type.getMethod("run"); + } + Object simpleConstructorLookup(Class type) throws Exception { + return type.getDeclaredConstructor(); + } + Object qualifiedGetClassLookup(Object object) throws Exception { + return object.getClass().getMethod("run"); + } + Object qualifiedGetClassConstructor(Object object) throws Exception { + return object.getClass().getDeclaredConstructor(); + } + Object spacedGetClassLookup(Object object) throws Exception { + return object.getClass ().getMethod("run").invoke(object); + } + Object spacedGetClassConstructor(Object object) throws Exception { + return object.getClass ( /* preserve lookup */ ).getDeclaredConstructor().newInstance(); + } + Object misleadingInvokeName(Object target, String invokeName) throws Exception { + return target.getClass().getMethod( + invokeName + ).invoke(target); + } + Object misleadingNewInstanceName(Object target, Class[] newInstance) throws Exception { + return target.getClass().getDeclaredConstructor( + newInstance + ).newInstance(); + } + Object twoMethodChains(Object first, Object second) throws Exception { + return consume( + first.getClass().getMethod("a").invoke(first), + second.getClass().getMethod("b").invoke(second)); + } + Object nestedMethodChain(Object target, String methodName) throws Exception { + return target.getClass().getMethod( + target.getClass().getMethod(methodName).getName() + ).invoke(target); + } + Object nestedConstructorChain(Object target, Class[] types) throws Exception { + return target.getClass().getDeclaredConstructor( + target.getClass().getDeclaredConstructor(types).getName() + ).newInstance(); + } + Object consume(Object first, Object second) { return first; } + Object aliasMethodLookup(Class klass, String name) throws Exception { + return klass.forName(name).getMethod("run").invoke(null); + } + Object aliasConstructorLookup(Class klass, String name) throws Exception { + return klass.forName(name).getDeclaredConstructor().newInstance(); + } + Object spacedVarMethodLookup(Object object) throws Exception { + var method = object.getClass ( /* infer Method */ ).getMethod("run"); + return method.invoke(object); + } + Object spacedVarConstructorLookup(Object object) throws Exception { + var constructor = object.getClass ( /* infer Constructor */ ).getDeclaredConstructor(); + return constructor.newInstance(); + } + Object spacedStandaloneVarMethodLookup(Object object) throws Exception { + var klass = object.getClass ( /* infer Class */ ); + return klass.getMethod("run"); + } + Object spacedStandaloneVarConstructorLookup(Object object) throws Exception { + var klass = object.getClass ( /* infer Class */ ); + return klass.getDeclaredConstructor(); + } + Object spacedClassLoaderLookup(String name) throws Exception { + var loader = String.class.getClassLoader ( /* infer ClassLoader */ ); + return loader.loadClass(name); + } + Object thisAliasMethodLookup(String name) throws Exception { + return this.classAlias.forName(name).getMethod("run").invoke(null); + } + Object thisAliasConstructorLookup(String name) throws Exception { + return this.classAlias.forName(name).getDeclaredConstructor().newInstance(); + } + Object thisAliasVarMethodLookup(String name) throws Exception { + var method = this.classAlias.forName(name).getMethod("run"); + return method.invoke(null); + } + Object thisAliasVarConstructorLookup(String name) throws Exception { + var constructor = this.classAlias.forName(name).getDeclaredConstructor(); + return constructor.newInstance(); + } + Object thisAliasDirectVarMethodLookup() throws Exception { + var method = this.classAlias.getMethod("run"); + return method.invoke(null); + } + Object thisAliasDirectVarConstructorLookup() throws Exception { + var constructor = this.classAlias.getDeclaredConstructor(); + return constructor.newInstance(); + } + Object thisAliasStandaloneVarMethodLookup(String name) throws Exception { + var loaded = this.classAlias.forName(name); + return loaded.getMethod("run"); + } + Object thisAliasStandaloneVarConstructorLookup(String name) throws Exception { + var loaded = this.classAlias.forName(name); + return loaded.getDeclaredConstructor(); + } + Object ordinaryThisAliasLookup(String name) throws Exception { + return this.ordinaryClassAlias.forName(name).getMethod("run"); + } + Object ordinaryThisAliasVarMethodLookup(String name) throws Exception { + var method = this.ordinaryClassAlias.forName(name).getMethod("run"); + return method.invoke(null); + } + Object ordinaryThisAliasVarConstructorLookup(String name) throws Exception { + var constructor = this.ordinaryClassAlias.forName(name).getDeclaredConstructor(); + return constructor.newInstance(); + } + Object ordinaryThisAliasDirectVarMethodLookup() throws Exception { + var method = this.ordinaryClassAlias.getMethod("run"); + return method.invoke(null); + } + Object ordinaryThisAliasDirectVarConstructorLookup() throws Exception { + var constructor = this.ordinaryClassAlias.getDeclaredConstructor(); + return constructor.newInstance(); + } + Object ordinaryThisAliasDirectChainMethodLookup() throws Exception { + return this.ordinaryClassAlias.getMethod("run").invoke(null); + } + Object ordinaryThisAliasDirectChainConstructorLookup() throws Exception { + return this.ordinaryClassAlias.getDeclaredConstructor().newInstance(); + } + Object ordinaryThisAliasStandaloneVarMethodLookup(String name) throws Exception { + var loaded = this.ordinaryClassAlias.forName(name); + return loaded.getMethod("run"); + } + Object ordinaryThisAliasStandaloneVarConstructorLookup(String name) throws Exception { + var loaded = this.ordinaryClassAlias.forName(name); + return loaded.getDeclaredConstructor(); + } + class Inner { + private Class ordinaryClassAlias; + Object outerAliasMethodLookup(String name) throws Exception { + return ChainedQualifiedShadowNames.this.classAlias + .forName(name).getMethod("run").invoke(null); + } + Object outerAliasConstructorLookup(String name) throws Exception { + return ChainedQualifiedShadowNames.this.classAlias + .forName(name).getDeclaredConstructor().newInstance(); + } + Object outerAliasVarMethodLookup(String name) throws Exception { + var method = ChainedQualifiedShadowNames.this.classAlias + .forName(name).getMethod("run"); + return method.invoke(null); + } + Object outerAliasVarConstructorLookup(String name) throws Exception { + var constructor = ChainedQualifiedShadowNames.this.classAlias + .forName(name).getDeclaredConstructor(); + return constructor.newInstance(); + } + Object outerAliasDirectVarMethodLookup() throws Exception { + var method = ChainedQualifiedShadowNames.this.classAlias.getMethod("run"); + return method.invoke(null); + } + Object outerAliasDirectVarConstructorLookup() throws Exception { + var constructor = ChainedQualifiedShadowNames.this.classAlias.getDeclaredConstructor(); + return constructor.newInstance(); + } + Object outerAliasStandaloneVarMethodLookup(String name) throws Exception { + var loaded = ChainedQualifiedShadowNames.this.classAlias.forName(name); + return loaded.getMethod("run"); + } + Object outerAliasStandaloneVarConstructorLookup(String name) throws Exception { + var loaded = ChainedQualifiedShadowNames.this.classAlias.forName(name); + return loaded.getDeclaredConstructor(); + } + Object outerOrdinaryAliasVarMethodLookup(String name) throws Exception { + var method = ChainedQualifiedShadowNames.this.ordinaryClassAlias + .forName(name).getMethod("run"); + return method.invoke(null); + } + Object outerOrdinaryAliasVarConstructorLookup(String name) throws Exception { + var constructor = ChainedQualifiedShadowNames.this.ordinaryClassAlias + .forName(name).getDeclaredConstructor(); + return constructor.newInstance(); + } + Object outerOrdinaryAliasDirectVarMethodLookup() throws Exception { + var method = ChainedQualifiedShadowNames.this.ordinaryClassAlias.getMethod("run"); + return method.invoke(null); + } + Object outerOrdinaryAliasDirectVarConstructorLookup() throws Exception { + var constructor = ChainedQualifiedShadowNames.this.ordinaryClassAlias.getDeclaredConstructor(); + return constructor.newInstance(); + } + Object outerOrdinaryAliasDirectChainMethodLookup() throws Exception { + return ChainedQualifiedShadowNames.this.ordinaryClassAlias.getMethod("run").invoke(null); + } + Object outerOrdinaryAliasDirectChainConstructorLookup() throws Exception { + return ChainedQualifiedShadowNames.this.ordinaryClassAlias + .getDeclaredConstructor().newInstance(); + } + Object outerOrdinaryAliasStandaloneVarMethodLookup(String name) throws Exception { + var loaded = ChainedQualifiedShadowNames.this.ordinaryClassAlias.forName(name); + return loaded.getMethod("run"); + } + Object outerOrdinaryAliasStandaloneVarConstructorLookup(String name) throws Exception { + var loaded = ChainedQualifiedShadowNames.this.ordinaryClassAlias.forName(name); + return loaded.getDeclaredConstructor(); + } + Object outerShadowedWhitespaceMethod(String name) throws Exception { + return ChainedQualifiedShadowNames. this.ordinaryClassAlias + .forName(name).getMethod("run").invoke(null); + } + Object outerShadowedCommentMethod(String name) throws Exception { + return ChainedQualifiedShadowNames. /* ordinary outer */ this.ordinaryClassAlias + .forName(name).getMethod("run").invoke(null); + } + Object outerShadowedWhitespaceConstructor(String name) throws Exception { + return ChainedQualifiedShadowNames. this.ordinaryClassAlias + .forName(name).getDeclaredConstructor().newInstance(); + } + Object outerShadowedCommentConstructor(String name) throws Exception { + return ChainedQualifiedShadowNames. /* ordinary outer */ this.ordinaryClassAlias + .forName(name).getDeclaredConstructor().newInstance(); + } + } + } + """); + write(sourceRoot, "demo/WildcardForName.java", """ + package demo; + import static java.lang.Class.*; + class WildcardForName { + Object load(String name) throws Exception { return forName(name); } + } + """); + write(sourceRoot, "demo/NestedInheritanceOuter.java", """ + package demo; + import java.lang.reflect.Method; + class NestedInheritanceOuter { + static class Base { protected Method action; } + static class Child extends Base { + Object run() throws Exception { return action.invoke(null); } + } + static class QualifiedChild extends NestedInheritanceOuter.Base { + Object run() throws Exception { return action.invoke(null); } + } + } + """); + write(sourceRoot, "demo/AOuter.java", """ + package demo; + import java.lang.reflect.Method; + class AOuter { + Method action; + class Inner extends ZBase { + Object run() { return action.invoke(); } + } + } + """); + write(sourceRoot, "demo/ZBase.java", """ + package demo; + class ZBase { Worker action; } + """); + write(sourceRoot, "demo/BOuter.java", """ + package demo; + class BOuter { + Worker action; + class Inner extends CBase { + Object run() { return action.invoke(); } + } + } + """); + write(sourceRoot, "demo/CBase.java", """ + package demo; + import java.lang.reflect.Method; + class CBase { Method action; } + """); + write(sourceRoot, "shadow/Class.java", """ + package shadow; + public class Class { + public static Object forName(String name) { return name; } + public Object getMethod(String name) { return name; } + } + """); + write(sourceRoot, "shadow/Method.java", """ + package shadow; + public class Method { + public Object invoke(Object... arguments) { return null; } + } + """); + write(sourceRoot, "shadow/ReceiverTypes.java", """ + package shadow; + class ReceiverTypes { + private Method method; + private Class classValue; + Object fields() { return method.invoke(null); } + Object classes() { return classValue.getMethod("run"); } + Object parameters(Method method, Class classValue) { + return method.invoke(null); + } + Object locals() { + Method method = new Method(); + Class classValue = new Class(); + return method.invoke(classValue); + } + } + """); + write(sourceRoot, "shadow/QualifiedReceiverTypes.java", """ + package shadow; + class QualifiedReceiverTypes { + private java.lang.reflect.Method method; + private java.lang.Class type; + private java.lang.ClassLoader loader; + private java.lang.reflect.Constructor constructor; + Object run(String name) throws Exception { + method.invoke(null); + type.getMethod("run"); + loader.loadClass(name); + constructor.newInstance(); + return null; + } + } + """); + write(sourceRoot, "shadow/InvocationHandler.java", """ + package shadow; + interface InvocationHandler { Object call(Object proxy, Payload value); } + """); + write(sourceRoot, "shadow/Payload.java", """ + package shadow; + interface Payload { Object invoke(); } + """); + write(sourceRoot, "shadow/HandlerUse.java", """ + package shadow; + class HandlerUse { + InvocationHandler handler = (proxy, value) -> value.invoke(); + Object run() { + InvocationHandler local = (proxy, value) -> value.invoke(); + return local; + } + } + """); + write(sourceRoot, "shadow/Proxy.java", """ + package shadow; + public class Proxy { + public static Object newProxyInstance(Object loader, Object interfaces, Object handler) { + return null; + } + } + """); + write(sourceRoot, "shadow/UsesShadowApis.java", """ + package shadow; + class UsesShadowApis { + Object classCall(String name) { return Class.forName(name); } + Object proxyCall() { return Proxy.newProxyInstance(null, null, null); } + } + """); + write(sourceRoot, "demo/InterfaceHandlers.java", """ + package demo; + interface InterfaceHandlers { + java.lang.reflect.InvocationHandler handler = + (proxy, method, args) -> method.invoke(null, args); + } + """); + write(sourceRoot, "demo/TextBlockExamples.java", + "package demo;\n" + + "public class TextBlockExamples {\n" + + " String examples = \"\"\"\n" + + " Class.forName(name);\n" + + " Proxy.newProxyInstance(loader, interfaces, handler);\n" + + " \"\"\";\n" + + "}\n"); + AnalysisContext context = context( + AnalysisContext.Framework.QUARKUS, + sourceRoot, + null, + List.of()); + + List dynamicFeatures = new SourceMetadataScanner(context).dynamicFeatures(); + SemanticCallFacts facts = new JavaAnalysisAdapter().analyze(context, new AnalysisGraph( + List.of(), + dynamicFeatures, + Set.of(), + "SootUp", + "3.0.1", + 0, + 0)); + List reflectionFacts = facts.providerFacts().stream() + .filter(item -> item.relationshipType().equals("reflection")) + .toList(); + assertTrue(reflectionFacts.size() >= 5); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#first"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#qualifiedReceivers"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#varReceiver") + && item.evidence().get(0).symbols().contains("Method.invoke"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#qualifiedVarReceiver") + && item.evidence().get(0).symbols().contains("Method.invoke"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#varLoader") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass"))); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().equals("StringPackageText#load") + && item.evidence().get(0).symbols().contains("Class.forName")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#urlClassLoader") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#customClassLoader") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#boundedParameter") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#boundedLocal") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#publicBounded") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#staticBounded") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#annotatedBounded") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#annotatedTypeBound") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#bitwiseAnnotatedBound") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#intersectionBound") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.AnnotatedGeneric#run") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().equals("demo.AnnotatedArrayGeneric#run") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().equals("demo.ComparisonGeneric#run") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().equals("demo.GenericBoundShadow#run") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#annotatedUnrelatedBound")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#unrelatedGeneric")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.WildcardLoader#load") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("#localTypeShadow")) + .count()); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#ordinaryInvoke")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.WildcardShadowMethod#invoke")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().equals("wildcard.UsesSamePackageMethod#invoke")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().equals("inherited.Child#ordinary")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().equals("inheritedprivate.Child#reflective")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().equals("inheritedchild.Child#ordinary")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().equals("lexicalprecedence.Outer$Inner#load") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().equals("hiddenmembers.C#load") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("localprecedence.LocalPrecedence$1Other#ordinary")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().equals("privatehide.C#reflective") + && item.evidence().get(0).symbols().contains("Method.invoke")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().equals("wildcard.WildcardUrlSubclass#load") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#varClassLiteral") + && item.evidence().get(0).symbols().contains("getMethod"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#varClassLoader") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass"))); + assertEquals(2, reflectionFacts.stream() + .filter(item -> item.caller().contains("#chainedLoader") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")) + .count()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#spacedVarReceiver") + && item.evidence().get(0).symbols().contains("Method.invoke"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#varUnrelated"))); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("#varScope") + && item.evidence().get(0).symbols().contains("Method.invoke")) + .count()); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#wrappedLookup") + && item.evidence().get(0).symbols().contains("Method.invoke"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#wrappedClass") + && item.evidence().get(0).symbols().contains("Method.invoke"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#second")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#chained") + && item.evidence().get(0).symbols().contains("Method.invoke"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#classLiteral"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#inlineInvocationProxy") + && item.evidence().get(0).symbols().contains("Method.invoke"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#inlineShortProxy") + && item.evidence().get(0).symbols().contains("Method.invoke"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#inlineTypedProxy") + && item.evidence().get(0).symbols().contains("Method.invoke"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .filter(item -> item.caller().equals("demo.DynamicSites#") + && item.evidence().get(0).symbols().contains("Method.invoke")) + .count() >= 4, reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .filter(item -> item.caller().equals("demo.DynamicSites#") + && item.evidence().get(0).symbols().contains("Method.invoke")) + .count() >= 2); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.DynamicSites#") + && item.evidence().get(0).symbols().contains("Class.forName"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.DynamicSites#") + && item.evidence().get(0).symbols().contains("Class.forName"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.InterfaceHandlers#") + && item.evidence().get(0).symbols().contains("Method.invoke"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.DynamicSites$1Local#") + && item.evidence().get(0).symbols().contains("Method.invoke")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("unknown#run") + && item.evidence().get(0).path().contains("DynamicSites.java") + && item.evidence().get(0).symbols().contains("Class.forName")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#constructionThenBlock") + && item.evidence().get(0).symbols().contains("Class.forName")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#anonymousConstructorArgument") + && item.evidence().get(0).symbols().contains("Class.forName")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#explicitAnonymousConstructorArgument") + && item.evidence().get(0).symbols().contains("Class.forName")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#annotatedAnonymousConstructorArgument") + && item.evidence().get(0).symbols().contains("Class.forName")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#spacedQualifiedAnonymousConstructorArgument") + && item.evidence().get(0).symbols().contains("Class.forName")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .filter(item -> item.caller().equals("unknown#run") + && item.evidence().get(0).symbols().contains("Class.forName")) + .count() >= 3, reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.Outer$Inner#run"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.Outer$Inner#"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.Outer$SiblingA#reflect"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.Outer$InheritedInner#reflectInherited")), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.Child#run"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.NestedInheritanceOuter$Child#run"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.NestedInheritanceOuter$QualifiedChild#run"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.AOuter$Inner#run"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.BOuter$Inner#run"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.CrossPackageFieldOuter$Inner#run"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.CrossPackageWorkerOuter$Inner#run"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("shadow.QualifiedReceiverTypes#run"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#characterLiteral"))); + assertTrue(reflectionFacts.stream().noneMatch(item -> item.callee().contains("done"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#ordinary"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.ChildWithShadow#run"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.ReverseOuter$Inner#ordinary"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#unrelated"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#unrelatedChained"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#unrelatedConstructor"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#fieldReceivers"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#multiFieldReceivers"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#initializedMultiFieldReceivers"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#comparisonMultiFieldReceivers"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#multiLocalReceivers"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#identifierComparisonMultiFieldReceivers"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#genericMultiFieldReceivers"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#identifierComparisonLocal"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#comparisonBothMultiFieldReceivers"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#comparisonBothLocal"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#finalMultiLocalReceivers"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#finalWorkerMultiLocal"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#annotatedArrayMultiLocal"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#annotatedNestedMultiLocal"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#annotatedArrayWorkerMultiLocal"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#annotatedFieldReceivers"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#nestedAnnotatedFieldReceivers"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#annotatedWorkerFieldReceivers"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#priorAnnotatedLocal"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#priorAnnotatedWorkerLocal"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#afterLambdaArgument"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#afterNestedLambdaArgument"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#afterLambdaWorkerArgument"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#nestedLambdaBlockScope"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#controlFlowLambdaBlockScope"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#nestedLambdaWorkerShadow"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#bracedForScope"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#unbracedForScope"))); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("#methodLocalForScope")) + .count()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#enhancedForScope"))); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("#enhancedForMethodScope")) + .count()); + assertEquals(2, reflectionFacts.stream() + .filter(item -> item.caller().contains("#enhancedForIterableScope")) + .count()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("#enhancedForMethodIterableScope")) + .count()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("#forIfElseScope")) + .count()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("#anonymousBranchScope")) + .count()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#switchForScope"))); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("#switchForMethodScope")) + .count()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#catchScope"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#catchWorkerScope"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#resourceScope"))); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("#resourceInitializerScope")) + .count()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("#resourceInitializerMethodScope")) + .count()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("#laterDeclaratorBefore")) + .count()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("#laterDeclaratorAfter")) + .count()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("#basicForMultiWorkerScope")) + .count()); + assertEquals(2, reflectionFacts.stream() + .filter(item -> item.caller().contains("#basicForMultiMethodScope")) + .count()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#forTryFinallyScope"))); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("#forTryMethodScope")) + .count()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("#forLabeledIfScope")) + .count()); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#multiFieldParameter"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#shadowed"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#unrelatedProxyName"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().contains("#ordinary"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.evidence().get(0).path().contains("TextBlockExamples.java"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.FakeStaticImportTextBlock#call")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().equals("demo.FakeExplicitTypeImportTextBlock#load") + && item.evidence().get(0).symbols().contains("Class.forName")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().equals("demo.FakeWildcardTypeImportTextBlock#load") + && item.evidence().get(0).symbols().contains("Class.forName")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ShadowedTypeNames#")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.JdkTypedTypeNames#fieldClass"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.JdkTypedTypeNames#thisFieldClass"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.JdkTypedTypeNames#parameterClass"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.JdkTypedTypeNames#aliasClass"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.caller().equals("demo.JdkTypedTypeNames#localClass"))); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.QualifiedShadowedTypeNames#")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.QualifiedThisOwners$Inner#outerClass")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.QualifiedThisJdkOwners$Inner#outerClass")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.QualifiedThisInheritedJdkOwners$Inner#outerClass")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.QualifiedThisHiddenOrdinaryOwners$Inner#outerClass")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.LocalQualifiedThisOwners$") + && item.caller().contains("#classCall")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.QualifiedThisOtherOrdinaryOwners$Inner#")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.QualifiedThisOtherJdkOwners$Inner#")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#invoke") + || item.caller().contains("demo.ChainedQualifiedShadowNames#construct") + || item.caller().contains("demo.ChainedQualifiedShadowNames#lookup") + || item.caller().contains("demo.ChainedQualifiedShadowNames#lookupConstructor")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#realLookup") + && item.evidence().get(0).symbols().contains("getMethod")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#realLookupConstructor") + && item.evidence().get(0).symbols().contains("getDeclaredConstructor")), + reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#simpleMethodLookup") + && item.evidence().get(0).symbols().contains("getMethod")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#simpleConstructorLookup") + && item.evidence().get(0).symbols().contains("getDeclaredConstructor")) + .count(), reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#qualifiedGetClassLookup") + && item.evidence().get(0).symbols().contains("getMethod")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#qualifiedGetClassConstructor") + && item.evidence().get(0).symbols().contains("getDeclaredConstructor")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#spacedGetClassLookup") + && item.evidence().get(0).symbols().contains("Method.invoke")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#spacedGetClassConstructor") + && item.evidence().get(0).symbols().contains("Constructor.newInstance")), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#misleadingInvokeName") + && item.evidence().get(0).symbols().contains("Method.invoke") + && item.evidence().get(0).line() == 61), + reflectionFacts.toString()); + assertTrue(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#misleadingNewInstanceName") + && item.evidence().get(0).symbols().contains("Constructor.newInstance") + && item.evidence().get(0).line() == 66), + reflectionFacts.toString()); + assertEquals(2, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#twoMethodChains") + && item.evidence().get(0).symbols().contains("Method.invoke")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#nestedMethodChain") + && item.evidence().get(0).symbols().contains("Method.invoke")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#nestedConstructorChain") + && item.evidence().get(0).symbols().contains("Constructor.newInstance")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#aliasMethodLookup") + && item.evidence().get(0).symbols().contains("getMethod")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#aliasMethodLookup") + && item.evidence().get(0).symbols().contains("Method.invoke")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#aliasConstructorLookup") + && item.evidence().get(0).symbols().contains("getDeclaredConstructor")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#aliasConstructorLookup") + && item.evidence().get(0).symbols().contains("Constructor.newInstance")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#spacedVarMethodLookup") + && item.evidence().get(0).symbols().contains("Method.invoke")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#spacedVarConstructorLookup") + && item.evidence().get(0).symbols().contains("Constructor.newInstance")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#spacedStandaloneVarMethodLookup") + && item.evidence().get(0).symbols().contains("getMethod")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#spacedStandaloneVarConstructorLookup") + && item.evidence().get(0).symbols().contains("getDeclaredConstructor")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#spacedClassLoaderLookup") + && item.evidence().get(0).symbols().contains("ClassLoader.loadClass")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#thisAliasMethodLookup") + && item.evidence().get(0).symbols().contains("getMethod")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#thisAliasMethodLookup") + && item.evidence().get(0).symbols().contains("Method.invoke")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#thisAliasConstructorLookup") + && item.evidence().get(0).symbols().contains("getDeclaredConstructor")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#thisAliasConstructorLookup") + && item.evidence().get(0).symbols().contains("Constructor.newInstance")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#thisAliasVarMethodLookup") + && item.evidence().get(0).symbols().contains("Method.invoke")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#thisAliasVarConstructorLookup") + && item.evidence().get(0).symbols().contains("Constructor.newInstance")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#thisAliasDirectVarMethodLookup") + && item.evidence().get(0).symbols().contains("Method.invoke")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#thisAliasDirectVarConstructorLookup") + && item.evidence().get(0).symbols().contains("Constructor.newInstance")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#thisAliasStandaloneVarMethodLookup") + && item.evidence().get(0).symbols().contains("getMethod")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#thisAliasStandaloneVarConstructorLookup") + && item.evidence().get(0).symbols().contains("getDeclaredConstructor")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerAliasMethodLookup") + && item.evidence().get(0).symbols().contains("Method.invoke")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerAliasConstructorLookup") + && item.evidence().get(0).symbols().contains("Constructor.newInstance")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerAliasVarMethodLookup") + && item.evidence().get(0).symbols().contains("Method.invoke")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerAliasVarConstructorLookup") + && item.evidence().get(0).symbols().contains("Constructor.newInstance")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerAliasDirectVarMethodLookup") + && item.evidence().get(0).symbols().contains("Method.invoke")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerAliasDirectVarConstructorLookup") + && item.evidence().get(0).symbols().contains("Constructor.newInstance")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerAliasStandaloneVarMethodLookup") + && item.evidence().get(0).symbols().contains("getMethod")) + .count(), reflectionFacts.toString()); + assertEquals(1, reflectionFacts.stream() + .filter(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerAliasStandaloneVarConstructorLookup") + && item.evidence().get(0).symbols().contains("getDeclaredConstructor")) + .count(), reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#ordinaryThisAliasLookup")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#ordinaryThisAliasVarMethodLookup")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#ordinaryThisAliasVarConstructorLookup")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#ordinaryThisAliasDirectVarMethodLookup")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#ordinaryThisAliasDirectVarConstructorLookup")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#ordinaryThisAliasDirectChainMethodLookup")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#ordinaryThisAliasDirectChainConstructorLookup")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerOrdinaryAliasVarMethodLookup")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerOrdinaryAliasVarConstructorLookup")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerOrdinaryAliasDirectVarMethodLookup")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerOrdinaryAliasDirectVarConstructorLookup")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerOrdinaryAliasDirectChainMethodLookup")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerOrdinaryAliasDirectChainConstructorLookup")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#ordinaryThisAliasStandaloneVarMethodLookup")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames#ordinaryThisAliasStandaloneVarConstructorLookup")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerOrdinaryAliasStandaloneVarMethodLookup")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerOrdinaryAliasStandaloneVarConstructorLookup")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerShadowedWhitespaceMethod")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerShadowedCommentMethod")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerShadowedWhitespaceConstructor")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream() + .anyMatch(item -> item.caller().contains("demo.ChainedQualifiedShadowNames$Inner#outerShadowedCommentConstructor")), + reflectionFacts.toString()); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.evidence().get(0).path().contains("StaticForNameShadow.java"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.evidence().get(0).path().contains("InheritedDynamicApiUse.java"))); + assertFalse(facts.providerFacts().stream().anyMatch(item -> item.evidence().get(0).path().contains("InheritedDynamicApiUse.java"))); + assertFalse(facts.providerFacts().stream().anyMatch(item -> item.evidence().get(0).path().contains("PrivateDynamicApiUse.java"))); + assertFalse(facts.providerFacts().stream().anyMatch(item -> item.evidence().get(0).path().contains("InheritedInterfaceDynamicApiUse.java"))); + assertFalse(facts.providerFacts().stream().anyMatch(item -> item.evidence().get(0).path().contains("GenericInheritedDynamicApiUse.java"))); + assertFalse(facts.providerFacts().stream().anyMatch(item -> item.evidence().get(0).path().contains("ExplicitImportedParentUse.java"))); + assertTrue(facts.providerFacts().stream().anyMatch(item -> item.caller().equals("demo.ImportedNestedParentUse#inheritedMethod") + && item.evidence().get(0).path().contains("ImportedNestedParentUse.java"))); + assertFalse(facts.providerFacts().stream().anyMatch(item -> item.evidence().get(0).path().contains("ImportedNestedInterfaceUse.java"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.evidence().get(0).path().contains("UsesShadowApis.java"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.evidence().get(0).path().contains("/ReceiverTypes.java"))); + assertFalse(reflectionFacts.stream().anyMatch(item -> item.evidence().get(0).path().contains("/HandlerUse.java"))); + assertTrue(reflectionFacts.stream().anyMatch(item -> item.evidence().get(0).path().contains("WildcardForName.java"))); + assertTrue(reflectionFacts.stream().allMatch(item -> + item.certainty() == SemanticCallFacts.Certainty.UNRESOLVED + && item.status() == SemanticCallFacts.Status.ABSTAINED + && item.evidence().get(0).line() != null)); + + List proxyFacts = facts.frameworkFacts().stream() + .filter(item -> item.relationshipType().equals("proxy_dispatch")) + .toList(); + assertEquals(17, proxyFacts.size(), proxyFacts.toString()); + assertTrue(proxyFacts.stream().anyMatch(item -> item.evidence().get(0).attributes().get("caller").contains("#proxy"))); + assertTrue(proxyFacts.stream().anyMatch(item -> item.evidence().get(0).attributes().get("caller").contains("#secondProxy"))); + assertTrue(proxyFacts.stream().anyMatch(item -> item.evidence().get(0).attributes().get("caller").contains("#proxyWithVariable") + && item.callee().equals("unknown#unknown"))); + assertTrue(proxyFacts.stream().anyMatch(item -> item.callee().equals("demo.Overloaded#run") + && item.evidence().get(0).attributes().get("targetDescriptor").equals("(String)"))); + assertTrue(proxyFacts.stream().anyMatch(item -> item.callee().equals("demo.Overloaded#run") + && item.evidence().get(0).attributes().get("targetDescriptor").equals("(int)"))); + assertFalse(proxyFacts.stream().anyMatch(item -> item.evidence().get(0).path().contains("TextBlockExamples.java"))); + assertFalse(proxyFacts.stream().anyMatch(item -> item.evidence().get(0).path().contains("ShadowedStatic.java"))); + assertFalse(proxyFacts.stream().anyMatch(item -> item.evidence().get(0).path().contains("UsesShadowApis.java"))); + assertFalse(proxyFacts.stream() + .anyMatch(item -> item.evidence().get(0).path().contains("FakeProxyImportTextBlock.java")), + proxyFacts.toString()); + assertFalse(proxyFacts.stream() + .anyMatch(item -> item.evidence().get(0).path().contains("ShadowedTypeNames.java")), + proxyFacts.toString()); + assertTrue(proxyFacts.stream().anyMatch(item -> item.evidence().get(0).path().contains("JdkTypedTypeNames.java") + && item.evidence().get(0).attributes().get("caller").contains("#fieldProxy"))); + assertTrue(proxyFacts.stream().anyMatch(item -> item.evidence().get(0).path().contains("JdkTypedTypeNames.java") + && item.evidence().get(0).attributes().get("caller").contains("#thisFieldProxy"))); + assertTrue(proxyFacts.stream().anyMatch(item -> item.evidence().get(0).path().contains("JdkTypedTypeNames.java") + && item.evidence().get(0).attributes().get("caller").contains("#parameterProxy"))); + assertTrue(proxyFacts.stream().anyMatch(item -> item.evidence().get(0).path().contains("JdkTypedTypeNames.java") + && item.evidence().get(0).attributes().get("caller").contains("#aliasProxy"))); + assertTrue(proxyFacts.stream().anyMatch(item -> item.evidence().get(0).path().contains("JdkTypedTypeNames.java") + && item.evidence().get(0).attributes().get("caller").contains("#localProxy"))); + assertFalse(proxyFacts.stream() + .anyMatch(item -> item.evidence().get(0).path().contains("QualifiedShadowedTypeNames.java")), + proxyFacts.toString()); + assertFalse(proxyFacts.stream() + .anyMatch(item -> item.evidence().get(0).path().contains("/demo/QualifiedThisOwners.java")), + proxyFacts.toString()); + assertTrue(proxyFacts.stream() + .anyMatch(item -> item.evidence().get(0).path().contains("QualifiedThisJdkOwners.java"))); + assertTrue(proxyFacts.stream() + .anyMatch(item -> item.evidence().get(0).path().contains("QualifiedThisInheritedJdkOwners.java"))); + assertFalse(proxyFacts.stream() + .anyMatch(item -> item.evidence().get(0).path().contains("QualifiedThisHiddenOrdinaryOwners.java")), + proxyFacts.toString()); + assertTrue(proxyFacts.stream() + .anyMatch(item -> item.evidence().get(0).path().contains("LocalQualifiedThisOwners.java"))); + List overloadedProviderFacts = facts.providerFacts().stream() + .filter(item -> item.callee().equals("demo.Overloaded#run")) + .toList(); + assertEquals(2, overloadedProviderFacts.size()); + assertTrue(overloadedProviderFacts.stream().allMatch(item -> + item.evidence().get(0).attributes().containsKey("targetDescriptor"))); + } + + /** Keeps repeated provider observations on one source line distinct without aborting the run. */ + @Test + void repeatedProviderCallSitesReceiveDistinctFactIds() throws IOException { + Path sourceRoot = temp.resolve("repeated-calls/src/main/java"); + write(sourceRoot, "demo/RepeatedCalls.java", "package demo; class RepeatedCalls {}\n"); + AnalysisContext context = context( + AnalysisContext.Framework.QUARKUS, + sourceRoot, + null, + List.of()); + MethodSymbol caller = new MethodSymbol("demo.RepeatedCalls", "run", "()V"); + MethodSymbol callee = new MethodSymbol("demo.Target", "call", "()V"); + CallEdge first = new CallEdge( + caller, callee, 4, "src/main/java/demo/RepeatedCalls.java", + SemanticCallFacts.Certainty.DEFINITE, "invokes"); + CallEdge second = new CallEdge( + caller, callee, 4, "src/main/java/demo/RepeatedCalls.java", + SemanticCallFacts.Certainty.DEFINITE, "invokes"); + + SemanticCallFacts facts = new JavaAnalysisAdapter().analyze(context, new AnalysisGraph( + List.of(first, second), List.of(), Set.of(), "SootUp", "3.0.1", 1, 2)); + assertEquals(2, facts.providerFacts().size()); + assertNotEquals(facts.providerFacts().get(0).id(), facts.providerFacts().get(1).id()); + } + + /** Preserves selected build context and generated roots in the serialized run metadata. */ + @Test + void runMetadataPreservesProfileScopeRootsFingerprintsAndLimitations() throws IOException { + Path sourceRoot = temp.resolve("metadata/src/test/java"); + Path generatedRoot = temp.resolve("metadata/target/generated-test-sources"); + Files.createDirectories(sourceRoot); + Files.createDirectories(generatedRoot); + AnalysisContext context = AnalysisContext.builder() + .id("spring-boot-test") + .framework(AnalysisContext.Framework.SPRING_BOOT) + .frameworkVersion("2.7.18") + .fixtureId("fsi-spring-boot") + .javaRelease(17) + .repositoryRoot(temp) + .applicationClasses(temp.resolve("metadata/classes")) + .selectedProfile("test") + .analysisScope(AnalysisContext.Scope.TEST) + .sourceRoot("src/test/java", sourceRoot, AnalysisContext.Scope.TEST, true) + .generatedSourceRoot( + "target/generated-test-sources", + generatedRoot, + AnalysisContext.Scope.TEST, + true, + false) + .dependencyClasspathRequired(false) + .provenance(new AnalysisContext.Provenance( + "https://example.test/repository.git", + "test", + "test-revision", + "source-fingerprint", + "generated-fingerprint", + "dependency-fingerprint", + "build-fingerprint", + "toolchain-fingerprint", + "analyzer-fingerprint", + "git-revision", + "provider-runtime", + "profile-digest", + "2026-01-01T00:00:00Z", + 60)) + .handoff(new AnalysisContext.Handoff( + "0.1.0", + "spring-boot-test", + "1.0.0", + "profile-digest", + "maven", + List.of("mvn", "-Ptest", "test"), + "target/application.jar", + "build-fingerprint", + List.of("dynamic behavior remains unresolved"))) + .build(); + + SemanticCallFacts facts = new JavaAnalysisAdapter().analyze(context, AnalysisGraph.empty()); + SemanticCallFacts.RunMetadata metadata = facts.runMetadata(); + assertEquals("test", metadata.selectedProfile()); + assertEquals("test", metadata.analysisScope()); + assertEquals(List.of(generatedRoot.toAbsolutePath().normalize()), metadata.generatedSourceRoots().stream() + .map(AnalysisContext.SourceRoot::path) + .toList()); + assertEquals("generated-fingerprint", metadata.provenance().generatedSourceFingerprint()); + assertEquals("profile-digest", metadata.provenance().profileDigest()); + assertEquals(List.of("dynamic behavior remains unresolved"), metadata.handoff().limitations()); + + Path output = temp.resolve("metadata/semantic-call-facts.json"); + SemanticCallFactsJsonWriter.write(facts, output); + String json = Files.readString(output); + assertTrue(json.contains("\"selectedProfile\" : \"test\"")); + assertTrue(json.contains("\"analysisScope\" : \"test\"")); + assertTrue(json.contains("\"generatedSourceRoots\"")); + assertTrue(json.contains(generatedRoot.toAbsolutePath().normalize().toString())); + assertTrue(json.contains("\"generatedSourceFingerprint\" : \"generated-fingerprint\"")); + assertTrue(json.contains("dynamic behavior remains unresolved")); + } + /** Builds the minimal production context used by adapter fixtures. */ private AnalysisContext context( AnalysisContext.Framework framework, diff --git a/src/test/java/com/projectlightpath/analysis/java/MethodSymbolTest.java b/src/test/java/com/projectlightpath/analysis/java/MethodSymbolTest.java new file mode 100644 index 0000000..695f7b2 --- /dev/null +++ b/src/test/java/com/projectlightpath/analysis/java/MethodSymbolTest.java @@ -0,0 +1,26 @@ +package com.projectlightpath.analysis.java; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class MethodSymbolTest { + @Test + void normalizesSootMethodSignaturesToJvmDescriptors() { + MethodSymbol method = MethodSymbol.fromSootSignature( + ""); + + assertEquals("demo.Service", method.owner()); + assertEquals("run", method.name()); + assertEquals("(Ljava/lang/String;I)Ljava/lang/String;", method.descriptor()); + } + + @Test + void normalizesConstructorsArraysAndNestedTypes() { + MethodSymbol constructor = MethodSymbol.fromSootSignature( + "(java.lang.String[], int[][])>"); + + assertEquals("", constructor.name()); + assertEquals("([Ljava/lang/String;[[I)V", constructor.descriptor()); + } +} diff --git a/src/test/java/com/projectlightpath/analysis/java/SootUpBaseGraphProviderIntegrationTest.java b/src/test/java/com/projectlightpath/analysis/java/SootUpBaseGraphProviderIntegrationTest.java new file mode 100644 index 0000000..b7f004c --- /dev/null +++ b/src/test/java/com/projectlightpath/analysis/java/SootUpBaseGraphProviderIntegrationTest.java @@ -0,0 +1,122 @@ +package com.projectlightpath.analysis.java; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import javax.tools.JavaCompiler; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class SootUpBaseGraphProviderIntegrationTest { + @TempDir Path temp; + + @Test + void analyzesCompiledApplicationWithNormalizedDescriptorsAndPossibleInterfaceDispatch() + throws IOException { + Path sourceRoot = temp.resolve("src/main/java"); + Path classes = temp.resolve("target/classes"); + write(sourceRoot, "demo/Service.java", """ + package demo; + public interface Service { + String run(String value); + } + """); + write(sourceRoot, "demo/Impl.java", """ + package demo; + public class Impl implements Service { + public String run(String value) { return value; } + } + """); + write(sourceRoot, "demo/App.java", """ + package demo; + public class App { + private final Impl field = new Impl(); + public String interfaceCall(Service service) { return service.run("x"); } + public String directCall(Impl implementation) { return implementation.run("x"); } + public String directFieldCall() { return field.run("x"); } + } + """); + compile(sourceRoot, classes); + + AnalysisContext context = AnalysisContext.builder() + .id("quarkus-production") + .framework(AnalysisContext.Framework.QUARKUS) + .frameworkVersion("3.40.1") + .fixtureId("sootup-integration") + .javaRelease(17) + .repositoryRoot(temp) + .applicationClasses(classes) + .selectedProfile("production") + .analysisScope(AnalysisContext.Scope.PRODUCTION) + .sourceRoot("src/main/java", sourceRoot, AnalysisContext.Scope.PRODUCTION, true) + .dependencyClasspathRequired(false) + .provenance(AnalysisContext.Provenance.forTesting()) + .build(); + + SemanticCallFacts facts = new JavaAnalysisAdapter().analyze(context); + + SemanticCallFacts.Fact interfaceDispatch = facts.providerFacts().stream() + .filter(fact -> fact.caller().equals("demo.App#interfaceCall")) + .filter(fact -> fact.relationshipType().equals("interface_dispatch")) + .findFirst() + .orElseThrow(() -> new AssertionError(facts.providerFacts().toString())); + assertEquals(SemanticCallFacts.Certainty.POSSIBLE, interfaceDispatch.certainty()); + assertEquals("(Ljava/lang/String;)Ljava/lang/String;", + interfaceDispatch.evidence().get(0).attributes().get("calleeSignature")); + + SemanticCallFacts.Fact directCall = facts.providerFacts().stream() + .filter(fact -> fact.caller().equals("demo.App#directCall")) + .filter(fact -> fact.relationshipType().equals("invokes")) + .filter(fact -> fact.callee().endsWith("#run")) + .findFirst() + .orElseThrow(() -> new AssertionError(facts.providerFacts().toString())); + assertEquals(SemanticCallFacts.Certainty.DEFINITE, directCall.certainty()); + assertEquals("(Ljava/lang/String;)Ljava/lang/String;", + directCall.evidence().get(0).attributes().get("calleeSignature")); + SemanticCallFacts.Fact directFieldCall = facts.providerFacts().stream() + .filter(fact -> fact.caller().equals("demo.App#directFieldCall")) + .filter(fact -> fact.relationshipType().equals("invokes")) + .filter(fact -> fact.callee().endsWith("#run")) + .findFirst() + .orElseThrow(() -> new AssertionError(facts.providerFacts().toString())); + assertEquals(SemanticCallFacts.Certainty.DEFINITE, directFieldCall.certainty()); + assertTrue(facts.providerFacts().stream().allMatch(fact -> + fact.evidence().get(0).attributes().get("calleeSignature").startsWith("("))); + } + + private static void compile(Path sourceRoot, Path classes) throws IOException { + Files.createDirectories(classes); + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + if (compiler == null) { + throw new IllegalStateException("a JDK compiler is required for the SootUp integration test"); + } + try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) { + List sources = Files.walk(sourceRoot) + .filter(path -> path.toString().endsWith(".java")) + .sorted() + .toList(); + var units = fileManager.getJavaFileObjectsFromFiles( + sources.stream().map(Path::toFile).toList()); + boolean compiled = compiler.getTask( + null, + fileManager, + null, + List.of("--release", "17", "-d", classes.toString()), + null, + units).call(); + assertTrue(compiled, "integration fixture compilation failed"); + } + } + + private static void write(Path root, String relative, String source) throws IOException { + Path path = root.resolve(relative); + Files.createDirectories(path.getParent()); + Files.writeString(path, source); + } +} From 427d2a628a8ff0ed1d3b01f742726981b3e87c71 Mon Sep 17 00:00:00 2001 From: Jordi Gil Date: Mon, 9 Nov 2026 10:41:19 -0500 Subject: [PATCH 04/10] fix: address review identity findings --- .../analysis/java/BoundedFrameworkModel.java | 36 ++++++--- .../java/SootUpBaseGraphProvider.java | 16 ++-- .../java/JavaAnalysisAdapterTest.java | 74 +++++++++++++++++++ 3 files changed, 112 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java b/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java index 51e04ae..d59c3bd 100644 --- a/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java +++ b/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java @@ -138,8 +138,19 @@ private List injectionFacts( String registryMetadata = framework == AnalysisContext.Framework.QUARKUS ? "quarkus-arc-bean-index" : "spring-bean-definition-registry"; - for (SourceMetadataScanner.InjectionPoint injection : scanner.injectionPoints(framework)) { - for (int parameterIndex = 0; parameterIndex < injection.parameters().size(); parameterIndex++) { + List injectionPoints = + scanner.injectionPoints(framework); + Map injectionCounts = new HashMap<>(); + for (SourceMetadataScanner.InjectionPoint injection : injectionPoints) { + injectionCounts.merge( + injection.file().simpleTypeName(), injection.parameters().size(), Integer::sum); + } + for (SourceMetadataScanner.InjectionPoint injection : injectionPoints) { + boolean uniqueInjectionSite = injectionCounts.getOrDefault( + injection.file().simpleTypeName(), 0) == 1; + for (int parameterIndex = 0; + parameterIndex < injection.parameters().size(); + parameterIndex++) { SourceMetadataScanner.Parameter parameter = injection.parameters().get(parameterIndex); List typeMatches = candidates.stream() .filter(candidate -> matches(context, scanner, injection.file(), parameter.type(), candidate)) @@ -195,6 +206,7 @@ private List injectionFacts( if (selectedRows.isEmpty()) { matches = bindingPreference(context, scanner, matches); } + boolean legacyIdAllowed = uniqueInjectionSite && matches.size() == 1; if (matches.isEmpty()) { facts.add(injectionFact( @@ -210,6 +222,7 @@ private List injectionFacts( List.of(), null, scanner, + legacyIdAllowed, "No active bounded bean candidate matches the injection type, qualifiers, profile, or binding metadata.")); continue; } @@ -281,6 +294,7 @@ && candidateMetadataActive(context, scanner, matches.get(0)) == Boolean.TRUE bindingRows, parameterIndex, scanner, + legacyIdAllowed, limitation)); } } @@ -302,13 +316,14 @@ private SemanticCallFacts.Fact injectionFact( List bindingRows, Integer parameterIndex, SourceMetadataScanner scanner, + boolean legacyIdAllowed, String limitation) { String owner = injection.file().typeName(); String target = candidate == null ? parameter.type() + "#" : candidate.target(); String callerPrefix = framework == AnalysisContext.Framework.QUARKUS ? "quarkus" : "spring"; - String id = injectionFactId(scanner, injection, parameter, candidate); + String id = injectionFactId(scanner, injection, parameter, candidate, legacyIdAllowed); String displayedQualifiers = String.join(",", displayQualifiers( mergeQualifiers(injection.qualifiers(), parameter.qualifiers()))); List evidence = new ArrayList<>(); @@ -319,9 +334,9 @@ private SemanticCallFacts.Fact injectionFact( injection.line(), Map.of( "parameter", parameter.type(), - "parameterIndex", parameterIndex == null ? "" : parameterIndex.toString(), - "injectionKind", injection.kind(), - "qualifiers", displayedQualifiers))); + "parameterIndex", parameterIndex == null ? "" : parameterIndex.toString(), + "injectionKind", injection.kind(), + "qualifiers", displayedQualifiers))); if (candidate != null) { Map attributes = new HashMap<>(); attributes.put("candidateCount", Integer.toString(candidateCount)); @@ -1532,7 +1547,8 @@ private String injectionFactId( SourceMetadataScanner scanner, SourceMetadataScanner.InjectionPoint injection, SourceMetadataScanner.Parameter parameter, - SourceMetadataScanner.BeanCandidate candidate) { + SourceMetadataScanner.BeanCandidate candidate, + boolean legacyIdAllowed) { String owner = injection.file().simpleTypeName(); String candidateName = candidate == null ? "unresolved" : candidate.file().simpleTypeName(); if (framework == AnalysisContext.Framework.QUARKUS @@ -1541,7 +1557,8 @@ private String injectionFactId( && !candidate.producer() && candidateName.equals("DefaultTransferService") && uniqueSimpleType(scanner, owner) - && uniqueSimpleType(scanner, candidateName)) { + && uniqueSimpleType(scanner, candidateName) + && legacyIdAllowed) { return "quarkus-resource-to-cdi-bean"; } if (framework == AnalysisContext.Framework.SPRING_BOOT @@ -1550,7 +1567,8 @@ && uniqueSimpleType(scanner, candidateName)) { && !candidate.producer() && candidateName.equals("TransactionService") && uniqueSimpleType(scanner, owner) - && uniqueSimpleType(scanner, candidateName)) { + && uniqueSimpleType(scanner, candidateName) + && legacyIdAllowed) { return "spring-controller-to-service-injection"; } String identity = String.join("\u0000", diff --git a/src/main/java/com/projectlightpath/analysis/java/SootUpBaseGraphProvider.java b/src/main/java/com/projectlightpath/analysis/java/SootUpBaseGraphProvider.java index 7c59532..8de857c 100644 --- a/src/main/java/com/projectlightpath/analysis/java/SootUpBaseGraphProvider.java +++ b/src/main/java/com/projectlightpath/analysis/java/SootUpBaseGraphProvider.java @@ -105,10 +105,11 @@ private static List applicationCalls( continue; } String sourcePath = sourcePath(context, caller.owner()); - String methodSource = scanner.methodSource(caller, call.getLineNumber()).orElse(""); - boolean serviceLoaderCall = scanner.sourceLineContains( - caller, call.getLineNumber(), "ServiceLoader.load") - || (call.getLineNumber() <= 0 && methodSource.contains("ServiceLoader.load")); + boolean serviceLoaderCall = call.getLineNumber() > 0 + ? scanner.sourceLineContains(caller, call.getLineNumber(), "ServiceLoader.load") + : scanner.methodSource(caller, call.getLineNumber()) + .map(source -> source.contains("ServiceLoader.load")) + .orElse(false); boolean interfaceDispatch = scanner.isInterfaceType(callee.owner()) || call.invokableStmt().getInvokeExpr() .map(expression -> expression instanceof JInterfaceInvokeExpr) @@ -129,7 +130,12 @@ private static List applicationCalls( certainty, relationshipType)); } - result.sort(Comparator.comparing(edge -> edge.caller().descriptor() + "->" + edge.callee().descriptor())); + result.sort(Comparator.comparing((CallEdge edge) -> edge.caller().displayName()) + .thenComparing(edge -> edge.caller().descriptor()) + .thenComparing(edge -> edge.callee().displayName()) + .thenComparing(edge -> edge.callee().descriptor()) + .thenComparingInt(CallEdge::line) + .thenComparing(CallEdge::relationshipType)); return List.copyOf(result); } diff --git a/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java b/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java index c0e827d..3d206ca 100644 --- a/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java +++ b/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java @@ -409,6 +409,80 @@ public void run(String... args) { } assertFalse(lifecycleFacts.stream().anyMatch(item -> item.id().equals("spring-boot-to-data-seeder"))); } + /** Avoids reusing legacy injection IDs when one owner has multiple injection sites. */ + @Test + void repeatedLegacyInjectionSitesReceiveDistinctFactIds() throws IOException { + Path quarkusSource = temp.resolve("repeated-injection-ids/quarkus/src/main/java"); + write(quarkusSource, "demo/TransferResource.java", """ + package demo; + import jakarta.inject.Inject; + import jakarta.ws.rs.Path; + @Path("/transfers") public class TransferResource { + @Inject DefaultTransferService first; + @Inject DefaultTransferService second; + } + """); + write(quarkusSource, "demo/DefaultTransferService.java", """ + package demo; + @jakarta.enterprise.context.ApplicationScoped + public class DefaultTransferService { } + """); + + SemanticCallFacts quarkusFacts = new JavaAnalysisAdapter().analyze( + context( + AnalysisContext.Framework.QUARKUS, + quarkusSource, + null, + List.of(new AnalysisContext.FrameworkMetadata( + "quarkus-cdi-annotations", + "source_annotations", + AnalysisContext.MetadataStatus.OBSERVED, + "src/main/java"))), + AnalysisGraph.empty()); + List quarkusInjections = quarkusFacts.frameworkFacts().stream() + .filter(item -> item.relationshipType().equals("dependency_injection")) + .toList(); + assertEquals(2, quarkusInjections.size()); + assertEquals(2, quarkusInjections.stream().map(SemanticCallFacts.Fact::id).distinct().count()); + assertTrue(quarkusInjections.stream().noneMatch( + item -> item.id().equals("quarkus-resource-to-cdi-bean"))); + + Path springSource = temp.resolve("repeated-injection-ids/spring/src/main/java"); + write(springSource, "demo/TransferController.java", """ + package demo; + import org.springframework.beans.factory.annotation.Autowired; + import org.springframework.web.bind.annotation.RestController; + @RestController public class TransferController { + @Autowired TransactionService first; + @Autowired TransactionService second; + } + """); + write(springSource, "demo/TransactionService.java", """ + package demo; + import org.springframework.stereotype.Service; + @Service public class TransactionService { } + """); + + SemanticCallFacts springFacts = new JavaAnalysisAdapter().analyze( + context( + AnalysisContext.Framework.SPRING_BOOT, + springSource, + null, + List.of(new AnalysisContext.FrameworkMetadata( + "spring-bean-annotations", + "source_annotations", + AnalysisContext.MetadataStatus.OBSERVED, + "src/main/java"))), + AnalysisGraph.empty()); + List springInjections = springFacts.frameworkFacts().stream() + .filter(item -> item.relationshipType().equals("dependency_injection")) + .toList(); + assertEquals(2, springInjections.size()); + assertEquals(2, springInjections.stream().map(SemanticCallFacts.Fact::id).distinct().count()); + assertTrue(springInjections.stream().noneMatch( + item -> item.id().equals("spring-controller-to-service-injection"))); + } + /** Enumerates Quarkus route paths and rejects method-only roots. */ @Test void quarkusRoutesEnumerateMethodPathsAndKeepMethodOnlyPaths() throws IOException { From bc84fe154be73ff8a24afb12b897d5fcf487b114 Mon Sep 17 00:00:00 2001 From: Jordi Gil Date: Mon, 9 Nov 2026 12:56:36 -0500 Subject: [PATCH 05/10] fix: include injection method signatures in IDs --- .../analysis/java/BoundedFrameworkModel.java | 12 +++-- .../java/JavaAnalysisAdapterTest.java | 49 +++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java b/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java index d59c3bd..4857a2e 100644 --- a/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java +++ b/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java @@ -140,13 +140,13 @@ private List injectionFacts( : "spring-bean-definition-registry"; List injectionPoints = scanner.injectionPoints(framework); - Map injectionCounts = new HashMap<>(); + Map injectionParameterCounts = new HashMap<>(); for (SourceMetadataScanner.InjectionPoint injection : injectionPoints) { - injectionCounts.merge( + injectionParameterCounts.merge( injection.file().simpleTypeName(), injection.parameters().size(), Integer::sum); } for (SourceMetadataScanner.InjectionPoint injection : injectionPoints) { - boolean uniqueInjectionSite = injectionCounts.getOrDefault( + boolean uniqueInjectionSite = injectionParameterCounts.getOrDefault( injection.file().simpleTypeName(), 0) == 1; for (int parameterIndex = 0; parameterIndex < injection.parameters().size(); @@ -1571,12 +1571,18 @@ && uniqueSimpleType(scanner, candidateName) && legacyIdAllowed) { return "spring-controller-to-service-injection"; } + String injectionSignature = injection.member() + "(" + + String.join(",", injection.parameters().stream() + .map(SourceMetadataScanner.Parameter::type) + .toList()) + + ")"; String identity = String.join("\u0000", framework.id(), injection.file().typeName(), injection.file().displayPath(), injection.member(), injection.kind(), + injectionSignature, parameter.name(), parameter.type(), candidate == null ? "unresolved" : candidate.candidateId(), diff --git a/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java b/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java index 3d206ca..8c06617 100644 --- a/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java +++ b/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java @@ -483,6 +483,55 @@ public class DefaultTransferService { } item -> item.id().equals("spring-controller-to-service-injection"))); } + /** Includes overloaded injection method signatures in fallback fact IDs. */ + @Test + void overloadedInjectionMethodsReceiveDistinctFactIds() throws IOException { + Path sourceRoot = temp.resolve("overloaded-injection-ids/quarkus/src/main/java"); + write(sourceRoot, "demo/TransferResource.java", """ + package demo; + import jakarta.inject.Inject; + import jakarta.ws.rs.Path; + @Path("/transfers") public class TransferResource { + @Inject void set(DefaultTransferService service) { } + @Inject void set(DefaultTransferService service, OtherService other) { } + } + """); + write(sourceRoot, "demo/DefaultTransferService.java", """ + package demo; + @jakarta.enterprise.context.ApplicationScoped + public class DefaultTransferService { } + """); + write(sourceRoot, "demo/OtherService.java", """ + package demo; + @jakarta.enterprise.context.ApplicationScoped + public class OtherService { } + """); + + SemanticCallFacts facts = new JavaAnalysisAdapter().analyze( + context( + AnalysisContext.Framework.QUARKUS, + sourceRoot, + null, + List.of(new AnalysisContext.FrameworkMetadata( + "quarkus-cdi-annotations", + "source_annotations", + AnalysisContext.MetadataStatus.OBSERVED, + "src/main/java"))), + AnalysisGraph.empty()); + List defaultServiceInjections = facts.frameworkFacts().stream() + .filter(item -> item.relationshipType().equals("dependency_injection")) + .filter(item -> item.callee().equals("demo.DefaultTransferService#")) + .toList(); + + assertEquals(2, defaultServiceInjections.size()); + assertEquals(2, defaultServiceInjections.stream() + .map(SemanticCallFacts.Fact::id) + .distinct() + .count()); + assertTrue(defaultServiceInjections.stream() + .noneMatch(item -> item.id().equals("quarkus-resource-to-cdi-bean"))); + } + /** Enumerates Quarkus route paths and rejects method-only roots. */ @Test void quarkusRoutesEnumerateMethodPathsAndKeepMethodOnlyPaths() throws IOException { From 8aa0f298a999afc3d8900b49c75b2dce419a9400 Mon Sep 17 00:00:00 2001 From: Jordi Gil Date: Mon, 9 Nov 2026 13:19:17 -0500 Subject: [PATCH 06/10] fix: disambiguate nested injection identities --- .../analysis/java/BoundedFrameworkModel.java | 12 +++-- .../analysis/java/SourceMetadataScanner.java | 10 ++++- .../java/JavaAnalysisAdapterTest.java | 45 +++++++++++++++++++ 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java b/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java index 4857a2e..37fabcc 100644 --- a/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java +++ b/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java @@ -143,11 +143,11 @@ private List injectionFacts( Map injectionParameterCounts = new HashMap<>(); for (SourceMetadataScanner.InjectionPoint injection : injectionPoints) { injectionParameterCounts.merge( - injection.file().simpleTypeName(), injection.parameters().size(), Integer::sum); + injection.ownerType(), injection.parameters().size(), Integer::sum); } for (SourceMetadataScanner.InjectionPoint injection : injectionPoints) { boolean uniqueInjectionSite = injectionParameterCounts.getOrDefault( - injection.file().simpleTypeName(), 0) == 1; + injection.ownerType(), 0) == 1; for (int parameterIndex = 0; parameterIndex < injection.parameters().size(); parameterIndex++) { @@ -318,7 +318,7 @@ private SemanticCallFacts.Fact injectionFact( SourceMetadataScanner scanner, boolean legacyIdAllowed, String limitation) { - String owner = injection.file().typeName(); + String owner = injection.ownerType(); String target = candidate == null ? parameter.type() + "#" : candidate.target(); @@ -1556,6 +1556,7 @@ private String injectionFactId( && candidate != null && !candidate.producer() && candidateName.equals("DefaultTransferService") + && injection.ownerType().equals(injection.file().typeName()) && uniqueSimpleType(scanner, owner) && uniqueSimpleType(scanner, candidateName) && legacyIdAllowed) { @@ -1566,6 +1567,7 @@ && uniqueSimpleType(scanner, candidateName) && candidate != null && !candidate.producer() && candidateName.equals("TransactionService") + && injection.ownerType().equals(injection.file().typeName()) && uniqueSimpleType(scanner, owner) && uniqueSimpleType(scanner, candidateName) && legacyIdAllowed) { @@ -1580,6 +1582,8 @@ && uniqueSimpleType(scanner, candidateName) framework.id(), injection.file().typeName(), injection.file().displayPath(), + injection.ownerType(), + Integer.toString(injection.sourceOffset()), injection.member(), injection.kind(), injectionSignature, @@ -1590,7 +1594,7 @@ && uniqueSimpleType(scanner, candidateName) candidate == null ? "" : candidate.producerSignature(), candidate == null ? "" : candidate.producerParameters()); return framework.id() + "-injection-" - + slug(injection.file().typeName() + "-" + injection.member() + "-" + parameter.type() + + slug(injection.ownerType() + "-" + injection.member() + "-" + parameter.type() + "-" + candidateName) + "-" + digest(identity).substring(0, 16); } diff --git a/src/main/java/com/projectlightpath/analysis/java/SourceMetadataScanner.java b/src/main/java/com/projectlightpath/analysis/java/SourceMetadataScanner.java index 72ec4b5..4447c6d 100644 --- a/src/main/java/com/projectlightpath/analysis/java/SourceMetadataScanner.java +++ b/src/main/java/com/projectlightpath/analysis/java/SourceMetadataScanner.java @@ -623,6 +623,8 @@ List injectionPoints(AnalysisContext.Framework framework) { file, parsedParameters, file.lineAt(constructorInfo.start()), + constructorInfo.start(), + typeContaining(file, constructorInfo.start()), "", "constructor", qualifierNames(constructorInfo.annotations()))); @@ -645,6 +647,8 @@ && hasAnnotation(file.classAnnotationBlock(), "Path"))) { file, List.of(parameter), file.lineAt(fieldMatcher.start()), + fieldMatcher.start(), + typeContaining(file, fieldMatcher.start()), fieldMatcher.group(3), "field", fieldQualifiers)); @@ -662,6 +666,8 @@ && hasAnnotation(file.classAnnotationBlock(), "Path"))) { file, parsedParameters, method.line(), + method.start(), + typeContaining(file, method.start()), method.name(), "method", methodQualifiers)); @@ -4911,11 +4917,13 @@ record InjectionPoint( SourceFile file, List parameters, int line, + int sourceOffset, + String ownerType, String member, String kind, List qualifiers) { InjectionPoint(SourceFile file, List parameters, int line) { - this(file, parameters, line, "", "constructor", List.of()); + this(file, parameters, line, 0, file.typeName(), "", "constructor", List.of()); } InjectionPoint { diff --git a/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java b/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java index 8c06617..7e37ec8 100644 --- a/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java +++ b/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java @@ -532,6 +532,51 @@ public class OtherService { } .noneMatch(item -> item.id().equals("quarkus-resource-to-cdi-bean"))); } + /** Includes nested injection owners and source offsets in fallback fact IDs. */ + @Test + void nestedInjectionSitesReceiveDistinctFactIds() throws IOException { + Path sourceRoot = temp.resolve("nested-injection-ids/quarkus/src/main/java"); + write(sourceRoot, "demo/TransferResource.java", """ + package demo; + import jakarta.inject.Inject; + import jakarta.ws.rs.Path; + @Path("/transfers") public class TransferResource { + static class First { + @Inject DefaultTransferService service; + } + static class Second { + @Inject DefaultTransferService service; + } + } + """); + write(sourceRoot, "demo/DefaultTransferService.java", """ + package demo; + @jakarta.enterprise.context.ApplicationScoped + public class DefaultTransferService { } + """); + + SemanticCallFacts facts = new JavaAnalysisAdapter().analyze( + context( + AnalysisContext.Framework.QUARKUS, + sourceRoot, + null, + List.of(new AnalysisContext.FrameworkMetadata( + "quarkus-cdi-annotations", + "source_annotations", + AnalysisContext.MetadataStatus.OBSERVED, + "src/main/java"))), + AnalysisGraph.empty()); + List injections = facts.frameworkFacts().stream() + .filter(item -> item.relationshipType().equals("dependency_injection")) + .filter(item -> item.callee().equals("demo.DefaultTransferService#")) + .toList(); + + assertEquals(2, injections.size()); + assertEquals(2, injections.stream().map(SemanticCallFacts.Fact::id).distinct().count()); + assertTrue(injections.stream().anyMatch(item -> item.caller().contains("demo.TransferResource$First"))); + assertTrue(injections.stream().anyMatch(item -> item.caller().contains("demo.TransferResource$Second"))); + } + /** Enumerates Quarkus route paths and rejects method-only roots. */ @Test void quarkusRoutesEnumerateMethodPathsAndKeepMethodOnlyPaths() throws IOException { From 7e1e8803977b789e90103066278d5a2926eb0361 Mon Sep 17 00:00:00 2001 From: Jordi Gil Date: Mon, 9 Nov 2026 14:19:54 -0500 Subject: [PATCH 07/10] fix: match bindings to nested injection owners --- .../analysis/java/BoundedFrameworkModel.java | 2 +- .../java/JavaAnalysisAdapterTest.java | 73 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java b/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java index 37fabcc..95eca37 100644 --- a/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java +++ b/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java @@ -1087,7 +1087,7 @@ private List matchingBindings( int parameterIndex, List candidates) { return context.resolvedBindings().stream() - .filter(row -> injectionOwnerMatches(scanner, row.injectionOwner(), injection.file().typeName())) + .filter(row -> injectionOwnerMatches(scanner, row.injectionOwner(), injection.ownerType())) .filter(row -> memberMatches(row.injectionMember(), injection.member())) .filter(row -> injectionTypesMatch(scanner, injection.file(), row.injectionType(), parameter.type())) .filter(row -> row.parameterIndex() == null || row.parameterIndex() == parameterIndex) diff --git a/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java b/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java index 7e37ec8..a3f47a1 100644 --- a/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java +++ b/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java @@ -577,6 +577,79 @@ public class DefaultTransferService { } assertTrue(injections.stream().anyMatch(item -> item.caller().contains("demo.TransferResource$Second"))); } + /** Matches resolved binding metadata against nested injection owners. */ + @Test + void resolvedBindingMatchesNestedInjectionOwner() throws IOException { + Path sourceRoot = temp.resolve("nested-binding-owner/quarkus/src/main/java"); + write(sourceRoot, "demo/TransferResource.java", """ + package demo; + import jakarta.inject.Inject; + import jakarta.ws.rs.Path; + @Path("/transfers") public class TransferResource { + static class First { + @Inject Service service; + } + } + """); + write(sourceRoot, "demo/Service.java", "package demo; public interface Service { }\n"); + write(sourceRoot, "demo/DefaultService.java", """ + package demo; + @jakarta.enterprise.context.ApplicationScoped + public class DefaultService implements Service { } + """); + write(sourceRoot, "demo/OtherService.java", """ + package demo; + @jakarta.enterprise.context.ApplicationScoped + public class OtherService implements Service { } + """); + + AnalysisContext.FrameworkBindingMetadata bindingMetadata = + new AnalysisContext.FrameworkBindingMetadata( + "0.1.0", + List.of( + new AnalysisContext.BindingCandidateMetadata( + "default", "demo.DefaultService", List.of("Service"), List.of(), "bean", + true, false, false), + new AnalysisContext.BindingCandidateMetadata( + "other", "demo.OtherService", List.of("Service"), List.of(), "bean", + true, false, false)), + List.of(new AnalysisContext.ResolvedBindingMetadata( + "first-service", + "demo.TransferResource$First", + "service", + "Service", + 0, + List.of(), + "default", + true, + true, + "selected nested binding", + null, + null)), + List.of()); + + SemanticCallFacts facts = new JavaAnalysisAdapter().analyze( + context( + AnalysisContext.Framework.QUARKUS, + sourceRoot, + null, + List.of(new AnalysisContext.FrameworkMetadata( + "quarkus-cdi-annotations", + "source_annotations", + AnalysisContext.MetadataStatus.OBSERVED, + "src/main/java")), + bindingMetadata), + AnalysisGraph.empty()); + List injections = facts.frameworkFacts().stream() + .filter(item -> item.relationshipType().equals("dependency_injection")) + .toList(); + + assertEquals(1, injections.size()); + assertEquals("demo.DefaultService#", injections.get(0).callee()); + assertEquals(SemanticCallFacts.Certainty.DEFINITE, injections.get(0).certainty()); + assertEquals("first-service", injections.get(0).evidence().get(1).attributes().get("bindingMetadataId")); + } + /** Enumerates Quarkus route paths and rejects method-only roots. */ @Test void quarkusRoutesEnumerateMethodPathsAndKeepMethodOnlyPaths() throws IOException { From 714d7ac662a0c0c084bd1b81365cc1bd126dcad4 Mon Sep 17 00:00:00 2001 From: Jordi Gil Date: Mon, 9 Nov 2026 14:38:21 -0500 Subject: [PATCH 08/10] fix: reject ambiguous nested binding owners --- .../analysis/java/BoundedFrameworkModel.java | 4 +- .../analysis/java/SourceMetadataScanner.java | 16 ++++ .../java/JavaAnalysisAdapterTest.java | 85 +++++++++++++++++++ 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java b/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java index 95eca37..25e1629 100644 --- a/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java +++ b/src/main/java/com/projectlightpath/analysis/java/BoundedFrameworkModel.java @@ -1124,8 +1124,8 @@ private static boolean metadataTypeMatches( return normalizedMetadata.equals(normalizedSource) || (!normalizedMetadata.contains(".") && normalizedMetadata.equals(simpleName(normalizedSource)) - && scanner.files().stream() - .filter(file -> file.simpleTypeName().equalsIgnoreCase(normalizedMetadata)) + && scanner.declaredTypeNames().stream() + .filter(type -> simpleName(type).equalsIgnoreCase(normalizedMetadata)) .count() <= 1); } diff --git a/src/main/java/com/projectlightpath/analysis/java/SourceMetadataScanner.java b/src/main/java/com/projectlightpath/analysis/java/SourceMetadataScanner.java index 4447c6d..bf81b41 100644 --- a/src/main/java/com/projectlightpath/analysis/java/SourceMetadataScanner.java +++ b/src/main/java/com/projectlightpath/analysis/java/SourceMetadataScanner.java @@ -161,6 +161,7 @@ final class SourceMetadataScanner { private final AnalysisContext context; private List files; + private Set declaredTypeNames; /** Creates a scanner over the selected analysis source roots. */ SourceMetadataScanner(AnalysisContext context) { @@ -192,6 +193,21 @@ List files() { return files; } + /** Returns fully qualified source owners, including nested declarations. */ + Set declaredTypeNames() { + if (declaredTypeNames != null) { + return declaredTypeNames; + } + Set result = new LinkedHashSet<>(); + for (SourceFile file : files()) { + for (TypeInfo type : typeDeclarations(file)) { + result.add(typeContaining(file, type.start())); + } + } + declaredTypeNames = Set.copyOf(result); + return declaredTypeNames; + } + /** Returns whether a source declaration is an interface in the selected roots. */ boolean isInterfaceType(String typeName) { return declaredType(typeName) diff --git a/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java b/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java index a3f47a1..2cb50a5 100644 --- a/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java +++ b/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java @@ -650,6 +650,91 @@ public class OtherService implements Service { } assertEquals("first-service", injections.get(0).evidence().get(1).attributes().get("bindingMetadataId")); } + /** Keeps an unqualified nested owner ambiguous across packages. */ + @Test + void ambiguousNestedOwnerMetadataDoesNotPromoteBindings() throws IOException { + Path sourceRoot = temp.resolve("ambiguous-nested-owner/quarkus/src/main/java"); + write(sourceRoot, "one/Outer.java", """ + package one; + import api.Service; + import jakarta.inject.Inject; + import jakarta.ws.rs.Path; + @Path("/one") public class Outer { + static class Nested { @Inject Service service; } + } + """); + write(sourceRoot, "two/Outer.java", """ + package two; + import api.Service; + import jakarta.inject.Inject; + import jakarta.ws.rs.Path; + @Path("/two") public class Outer { + static class Nested { @Inject Service service; } + } + """); + write(sourceRoot, "api/Service.java", "package api; public interface Service { }\n"); + write(sourceRoot, "one/OneService.java", """ + package one; + @jakarta.enterprise.context.ApplicationScoped + public class OneService implements api.Service { } + """); + write(sourceRoot, "two/TwoService.java", """ + package two; + @jakarta.enterprise.context.ApplicationScoped + public class TwoService implements api.Service { } + """); + + AnalysisContext.FrameworkBindingMetadata bindingMetadata = + new AnalysisContext.FrameworkBindingMetadata( + "0.1.0", + List.of( + new AnalysisContext.BindingCandidateMetadata( + "one-service", "one.OneService", List.of("api.Service", "Service"), List.of(), "bean", + true, false, false), + new AnalysisContext.BindingCandidateMetadata( + "two-service", "two.TwoService", List.of("api.Service", "Service"), List.of(), "bean", + true, false, false)), + List.of(new AnalysisContext.ResolvedBindingMetadata( + "ambiguous-nested-binding", + "Outer$Nested", + "service", + "Service", + null, + List.of(), + "one-service", + true, + true, + "ambiguous unqualified nested owner", + null, + null)), + List.of()); + + SemanticCallFacts facts = new JavaAnalysisAdapter().analyze( + context( + AnalysisContext.Framework.QUARKUS, + sourceRoot, + null, + List.of(new AnalysisContext.FrameworkMetadata( + "quarkus-cdi-annotations", + "source_annotations", + AnalysisContext.MetadataStatus.OBSERVED, + "src/main/java")), + bindingMetadata), + AnalysisGraph.empty()); + List injections = facts.frameworkFacts().stream() + .filter(item -> item.relationshipType().equals("dependency_injection")) + .toList(); + + assertEquals(4, injections.size()); + assertTrue(injections.stream() + .allMatch(item -> item.certainty() == SemanticCallFacts.Certainty.POSSIBLE)); + assertEquals(2, injections.stream() + .map(SemanticCallFacts.Fact::caller) + .map(caller -> caller.substring(0, caller.indexOf('#'))) + .distinct() + .count()); + } + /** Enumerates Quarkus route paths and rejects method-only roots. */ @Test void quarkusRoutesEnumerateMethodPathsAndKeepMethodOnlyPaths() throws IOException { From eb3a484df6067160bf3167fc9c6f17e6a3bfb560 Mon Sep 17 00:00:00 2001 From: Jordi Gil Date: Tue, 10 Nov 2026 08:41:24 -0500 Subject: [PATCH 09/10] fix: make provider fact IDs collision-stable --- .../analysis/java/ProviderFactNormalizer.java | 31 ++++++++++--------- .../java/JavaAnalysisAdapterTest.java | 21 ++++++++++--- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/projectlightpath/analysis/java/ProviderFactNormalizer.java b/src/main/java/com/projectlightpath/analysis/java/ProviderFactNormalizer.java index d4a96f1..d095f9f 100644 --- a/src/main/java/com/projectlightpath/analysis/java/ProviderFactNormalizer.java +++ b/src/main/java/com/projectlightpath/analysis/java/ProviderFactNormalizer.java @@ -4,8 +4,10 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -16,13 +18,18 @@ private ProviderFactNormalizer() {} static List normalize(AnalysisContext context, AnalysisGraph graph) { List facts = new ArrayList<>(); Set ids = new HashSet<>(); + Map baseIdCounts = new HashMap<>(); + for (CallEdge edge : graph.calls()) { + baseIdCounts.merge(legacyCallId(edge), 1, Integer::sum); + } for (CallEdge edge : graph.calls()) { String callIdentity = edge.caller().displayName() + "|" + edge.caller().descriptor() + "->" + edge.callee().displayName() + "|" + edge.callee().descriptor() + "@" + edge.line() + "|" + edge.sourcePath() + "|" + edge.relationshipType(); - String baseId = "sootup-call-" + Integer.toHexString( - (edge.caller().descriptor() + "->" + edge.callee().descriptor() + "@" + edge.line()).hashCode()); - String id = uniqueId(ids, baseId, callIdentity); + String baseId = legacyCallId(edge); + String id = baseIdCounts.get(baseId) == 1 + ? uniqueId(ids, baseId, callIdentity) + : uniqueId(ids, baseId + "-" + sha256(callIdentity), callIdentity); boolean openWorldDynamic = isOpenWorldDynamic(edge.relationshipType()); SemanticCallFacts.Certainty certainty = openWorldDynamic ? SemanticCallFacts.Certainty.UNRESOLVED @@ -83,17 +90,13 @@ private static String dynamicFeatureId(DynamicFeature feature) { + feature.sourceLine() + "|" + feature.sourceOffset() + "|" + feature.symbols(); - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] bytes = digest.digest(identity.getBytes(StandardCharsets.UTF_8)); - StringBuilder hex = new StringBuilder(bytes.length * 2); - for (byte value : bytes) { - hex.append(String.format("%02x", value)); - } - return "sootup-" + feature.kind().name().toLowerCase() + "-" + hex; - } catch (NoSuchAlgorithmException error) { - throw new IllegalStateException("SHA-256 is unavailable", error); - } + return "sootup-" + feature.kind().name().toLowerCase(Locale.ROOT) + "-" + sha256(identity); + } + + /** Returns the legacy provider ID seed used for ordinary call edges. */ + private static String legacyCallId(CallEdge edge) { + return "sootup-call-" + Integer.toHexString( + (edge.caller().descriptor() + "->" + edge.callee().descriptor() + "@" + edge.line()).hashCode()); } /** Keeps legacy IDs stable for ordinary edges while making collisions explicit and safe. */ diff --git a/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java b/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java index 2cb50a5..7dd7fa2 100644 --- a/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java +++ b/src/test/java/com/projectlightpath/analysis/java/JavaAnalysisAdapterTest.java @@ -5141,9 +5141,9 @@ interface InterfaceHandlers { item.evidence().get(0).attributes().containsKey("targetDescriptor"))); } - /** Keeps repeated provider observations on one source line distinct without aborting the run. */ + /** Keeps colliding provider identities stable without reserving the legacy ID for one edge. */ @Test - void repeatedProviderCallSitesReceiveDistinctFactIds() throws IOException { + void collidingProviderBaseIdsAreStableAndNeverUseLegacyId() throws IOException { Path sourceRoot = temp.resolve("repeated-calls/src/main/java"); write(sourceRoot, "demo/RepeatedCalls.java", "package demo; class RepeatedCalls {}\n"); AnalysisContext context = context( @@ -5154,16 +5154,29 @@ void repeatedProviderCallSitesReceiveDistinctFactIds() throws IOException { MethodSymbol caller = new MethodSymbol("demo.RepeatedCalls", "run", "()V"); MethodSymbol callee = new MethodSymbol("demo.Target", "call", "()V"); CallEdge first = new CallEdge( - caller, callee, 4, "src/main/java/demo/RepeatedCalls.java", + caller, callee, 4, "src/main/java/demo/First.java", SemanticCallFacts.Certainty.DEFINITE, "invokes"); CallEdge second = new CallEdge( - caller, callee, 4, "src/main/java/demo/RepeatedCalls.java", + caller, callee, 4, "src/main/java/demo/Second.java", SemanticCallFacts.Certainty.DEFINITE, "invokes"); SemanticCallFacts facts = new JavaAnalysisAdapter().analyze(context, new AnalysisGraph( List.of(first, second), List.of(), Set.of(), "SootUp", "3.0.1", 1, 2)); + SemanticCallFacts reversedFacts = new JavaAnalysisAdapter().analyze(context, new AnalysisGraph( + List.of(second, first), List.of(), Set.of(), "SootUp", "3.0.1", 1, 2)); + String legacyId = "sootup-call-" + Integer.toHexString( + (caller.descriptor() + "->" + callee.descriptor() + "@4").hashCode()); + assertEquals(2, facts.providerFacts().size()); assertNotEquals(facts.providerFacts().get(0).id(), facts.providerFacts().get(1).id()); + assertTrue(facts.providerFacts().stream().noneMatch(fact -> fact.id().equals(legacyId))); + Map idsByPath = facts.providerFacts().stream() + .collect(java.util.stream.Collectors.toMap( + fact -> fact.evidence().get(0).path(), SemanticCallFacts.Fact::id)); + Map reversedIdsByPath = reversedFacts.providerFacts().stream() + .collect(java.util.stream.Collectors.toMap( + fact -> fact.evidence().get(0).path(), SemanticCallFacts.Fact::id)); + assertEquals(idsByPath, reversedIdsByPath); } /** Preserves selected build context and generated roots in the serialized run metadata. */ From d268500cb5f906265fd0e496aacba1e4dbc5740a Mon Sep 17 00:00:00 2001 From: Jordi Gil Date: Tue, 10 Nov 2026 08:42:37 -0500 Subject: [PATCH 10/10] fix: classify direct interface calls --- .../analysis/java/SootUpBaseGraphProvider.java | 7 +++---- .../java/SootUpBaseGraphProviderIntegrationTest.java | 11 +++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/projectlightpath/analysis/java/SootUpBaseGraphProvider.java b/src/main/java/com/projectlightpath/analysis/java/SootUpBaseGraphProvider.java index 8de857c..8734a8d 100644 --- a/src/main/java/com/projectlightpath/analysis/java/SootUpBaseGraphProvider.java +++ b/src/main/java/com/projectlightpath/analysis/java/SootUpBaseGraphProvider.java @@ -110,10 +110,9 @@ private static List applicationCalls( : scanner.methodSource(caller, call.getLineNumber()) .map(source -> source.contains("ServiceLoader.load")) .orElse(false); - boolean interfaceDispatch = scanner.isInterfaceType(callee.owner()) - || call.invokableStmt().getInvokeExpr() - .map(expression -> expression instanceof JInterfaceInvokeExpr) - .orElse(false); + boolean interfaceDispatch = call.invokableStmt().getInvokeExpr() + .map(expression -> expression instanceof JInterfaceInvokeExpr) + .orElseGet(() -> scanner.isInterfaceType(callee.owner())); String relationshipType = serviceLoaderCall ? "service_loader" : interfaceDispatch diff --git a/src/test/java/com/projectlightpath/analysis/java/SootUpBaseGraphProviderIntegrationTest.java b/src/test/java/com/projectlightpath/analysis/java/SootUpBaseGraphProviderIntegrationTest.java index b7f004c..685d5f8 100644 --- a/src/test/java/com/projectlightpath/analysis/java/SootUpBaseGraphProviderIntegrationTest.java +++ b/src/test/java/com/projectlightpath/analysis/java/SootUpBaseGraphProviderIntegrationTest.java @@ -25,6 +25,7 @@ void analyzesCompiledApplicationWithNormalizedDescriptorsAndPossibleInterfaceDis package demo; public interface Service { String run(String value); + static String staticRun(String value) { return value; } } """); write(sourceRoot, "demo/Impl.java", """ @@ -38,6 +39,7 @@ public class Impl implements Service { public class App { private final Impl field = new Impl(); public String interfaceCall(Service service) { return service.run("x"); } + public String interfaceStaticCall() { return Service.staticRun("x"); } public String directCall(Impl implementation) { return implementation.run("x"); } public String directFieldCall() { return field.run("x"); } } @@ -86,6 +88,15 @@ public class App { .findFirst() .orElseThrow(() -> new AssertionError(facts.providerFacts().toString())); assertEquals(SemanticCallFacts.Certainty.DEFINITE, directFieldCall.certainty()); + SemanticCallFacts.Fact interfaceStaticCall = facts.providerFacts().stream() + .filter(fact -> fact.caller().equals("demo.App#interfaceStaticCall")) + .filter(fact -> fact.callee().equals("demo.Service#staticRun")) + .filter(fact -> fact.relationshipType().equals("invokes")) + .findFirst() + .orElseThrow(() -> new AssertionError(facts.providerFacts().toString())); + assertEquals(SemanticCallFacts.Certainty.DEFINITE, interfaceStaticCall.certainty()); + assertEquals("(Ljava/lang/String;)Ljava/lang/String;", + interfaceStaticCall.evidence().get(0).attributes().get("calleeSignature")); assertTrue(facts.providerFacts().stream().allMatch(fact -> fact.evidence().get(0).attributes().get("calleeSignature").startsWith("("))); }