Summary
Add a new repo-driven analysis output to Native Build Tools that emits, for the current resolved classpath, only the methods whose parameters participate in dynamic-access calls.
The task/goal reads precomputed method-level JSON from the configured reachability metadata repository, resolves the effective metadata version for each classpath artifact using the same repository-selection rules already used for reachability metadata, filters to the current project classpath, and writes a standalone JSON artifact.
The method-level JSON in the repository is produced by agent-based static analysis of library source code in the metadata repository pipeline. Native Build Tools consumes this pre-validated data without re-analyzing bytecode.
Native Build Tools performs local agent-based analysis of user source code to:
- Correlate call sites where user code invokes exposed third-pary library methods that perform dynamic access
- Detect direct dynamic access calls in user code itself and expose those to the third-party libs
This provides a complete picture: library exposures from the repository + user code exposures and call sites from local analysis.
Workflow Diagram
┌─────────────────────────────────────────────────────────────────────────────┐
│ INPUTS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────────────────┐ │
│ │ Project Classpath │ │ Metadata Repository │ │
│ │ │ │ │ │
│ │ • lib-a:1.2.3.jar │ │ exposed-dynamic-access/ │ │
│ │ • lib-b:4.5.6.jar │ │ └─ <group>/<artifact>/<version>/ │ │
│ │ • lib-c:7.8.9.jar │ │ └─ exposed-dynamic-access.json│ │
│ └───────────┬─────────────┘ └──────────────────┬──────────────────┘ │
│ │ │ │
└──────────────┼───────────────────────────────────────┼──────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ PROCESSING │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Resolve metadata version per artifact │
│ (reuses existing reachability metadata version resolution) │
│ │ │
│ ▼ │
│ 2. Fetch exposed-dynamic-access.json for each artifact from repository │
│ (skip artifacts without repo data) │
│ │ │
│ ▼ │
│ 3. Agent-based source analysis │
│ • Correlate: find call sites in user code that invoke exposed methods │
│ • Intrinsic: detect direct dynamic access calls in user code │
│ │ │
│ ▼ │
│ 4. Merge & aggregate by class, deterministic ordering │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ OUTPUT │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ exposed-dynamic-access.json │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "schemaVersion": "1.0.0", │ │
│ │ "repositoryAvailable": true, │ │
│ │ "inputs": [ { artifact, metadataVersion }, ... ], │ │
│ │ "classes": [ │ │
│ │ { "className": "...", "origin": "repository|source_analysis", │ │
│ │ "methods": [ │ │
│ │ { "signature": "...", "parameters": [ │ │
│ │ { "index": 0, "influences": [ │ │
│ │ { dependencyKind, dynamicAccessType, trackedApi } │ │
│ │ ]} │ │
│ │ ]} │ │
│ │ ]} │ │
│ │ ], │ │
│ │ "callSites": [ ... ] │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ Location: │
│ Gradle: build/generated/<task-name>/exposed-dynamic-access.json │
│ Maven: target/exposed-dynamic-access.json │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Key Changes
Public interfaces
- Add Gradle per-binary tasks mirroring existing native-image task naming:
generateExposedDynamicAccess
- Same pattern for any additional registered binaries
- Add a Maven goal:
generateExposedDynamicAccess
- Default outputs:
- Gradle:
build/generated/<task-name>/exposed-dynamic-access.json
- Maven:
target/exposed-dynamic-access.json
Output JSON
The task produces a single aggregated output for the entire classpath, with methods grouped by class and merged from all artifacts that have parameter influence data:
{
"schemaVersion": "1.0.0",
"repositoryAvailable": true,
"inputs": [
{
"artifact": "com.example:library:1.2.3",
"metadataVersion": "1.2.0"
},
{
"artifact": "org.other:util:4.5.6",
"metadataVersion": "4.5.0"
}
],
"classes": [
{
"className": "com.example.Library",
"origin": "repository",
"methods": [
{
"signature": "load(java.lang.String, boolean) -> java.lang.Class",
"parameters": [
{
"index": 0,
"influences": [
{
"dependencyKind": "direct",
"dynamicAccessType": "reflection",
"trackedApi": "java.lang.Class#forName(java.lang.String)"
},
{
"dependencyKind": "derived",
"dynamicAccessType": "reflection",
"trackedApi": "java.lang.Class#getDeclaredMethod(java.lang.String,java.lang.Class[])"
}
]
},
{
"index": 1,
"influences": [
{
"dependencyKind": "control_flow",
"dynamicAccessType": "reflection",
"trackedApi": "java.lang.Class#forName(java.lang.String)"
}
]
}
]
}
]
}
],
"callSites": []
}
Rules:
inputs lists only artifacts that contributed parameter influence data (had exposed-dynamic-access.json in repo).
inputs[*].artifact is groupId:artifactId:version from the classpath.
inputs[*].metadataVersion is the resolved metadata version used to read exposed-dynamic-access.json.
classes[*].origin is "repository" for library exposures from the metadata repository, or "source_analysis" for exposures detected in user code by local analysis.
callSites contains correlation data from source analysis (empty array when source analysis is disabled).
- Only classes with at least one method having influenced parameters appear.
- Only methods with at least one influenced parameter appear.
- Parameter indices are zero-based.
dependencyKind is a closed set. See Dependency kind semantics below.
dynamicAccessType is pass-through from repo data; do not hard-code it beyond string transport.
- If the metadata repository is unavailable or disabled, still write a valid file with
repositoryAvailable: false, inputs: [], classes: [], and callSites: [].
Dependency kind semantics
dependencyKind must be one of direct, derived, or control_flow. Native Build Tools treats this as repo-authored classification data and must reject or ignore any unexpected value rather than inventing a fallback category.
direct means the parameter value itself is used as the dynamic-access input, or it flows to that input without any material transformation. Typical cases are passing a String parameter directly to Class.forName, using a parameter as the member name in reflective lookup, or forwarding the same value through simple aliases.
derived means the parameter still influences the dynamic-access input, but only after an intermediate transformation, lookup, extraction, or composition step. Typical cases are deriving a class name from a parameter object, reading a reflective target from a map keyed by the parameter, or concatenating a package prefix with the parameter before the reflective call.
control_flow means the parameter does not supply the dynamic-access operand itself, but it decides whether a dynamic-access call happens or which precomputed target is chosen. Typical cases are boolean flags, enums, or mode strings that select between branches where each branch performs different reflection/resource/proxy access.
- Classification is per influence record, not per parameter globally. The same parameter may legitimately produce multiple influence entries if it affects different tracked APIs in different ways.
- When a situation could fit more than one category, prefer the most direct explanation of how the parameter affects the tracked API input:
- Use
direct when the tracked API consumes the parameter value itself.
- Use
derived when the tracked API consumes a value computed from the parameter.
- Use
control_flow when the parameter only selects the path that eventually performs dynamic access.
Classification decision rule
The classification depends on data flow to the API operand, not on whether the parameter appears in control flow constructs. The key question is: does the parameter's VALUE flow to the tracked API, or does the parameter merely SELECT which predetermined value gets used?
| Parameter appears in condition? |
API operand is... |
Classification |
| Yes or No |
Parameter reference (className) |
direct |
| Yes or No |
String literal ("com.example.Foo") |
control_flow |
| Yes or No |
Computed from param (name + ".Impl") |
derived |
Examples
Example 1: Pure direct — no control flow
void process(String className) {
Class.forName(className);
}
Classification: direct — the parameter IS the operand.
Example 2: Direct with validation guard
void process(String className) {
if (isAllowedClass(className)) {
Class.forName(className); // ← parameter reference
}
}
Classification: direct — despite the conditional guard, the parameter value itself flows to Class.forName. The control flow is validation/filtering, not target selection.
Example 3: Parameter in switch, passed directly in each case
void process(String type) {
switch(type) {
case "A": Class.forName(type); break; // ← parameter reference
case "B": Class.forName(type); break; // ← parameter reference
}
}
Classification: direct — the switch constrains possible values, but the parameter itself flows to the API. The operand is type, not a literal.
Example 4: Control flow selecting hardcoded targets
void process(String mode) {
if (mode.equals("debug")) {
Class.forName("com.example.DebugHandler"); // ← string literal
} else {
Class.forName("com.example.ReleaseHandler"); // ← string literal
}
}
Classification: control_flow — the parameter selects the branch, but the reflection targets are hardcoded literals. Native Image can statically enumerate both targets.
Example 5: Condition values match literals (no data flow)
void process(String className) {
if (className.equals("com.example.Foo")) {
Class.forName("com.example.Foo"); // ← string literal, not parameter
} else if (className.equals("com.example.Bar")) {
Class.forName("com.example.Bar"); // ← string literal, not parameter
}
}
Classification: control_flow — even though the literals coincidentally match the condition values, there is no data flow from parameter to operand. The operands are string literals. The parameter merely selects which literal gets used.
Example 6: Boolean/enum parameter
void process(boolean useDebug) {
if (useDebug) {
Class.forName("DebugImpl");
} else {
Class.forName("ProdImpl");
}
}
Classification: control_flow — the parameter cannot possibly be the class name (wrong type). It purely selects which hardcoded target is used.
Example 7: Derived with control flow guard
void process(String module) {
if (module.equals("core") || module.equals("ext")) {
Class.forName("com." + module + ".Main"); // ← concatenation
}
}
Classification: derived — the operand is computed from the parameter via string concatenation, not the parameter itself.
Example 8: Same parameter, multiple influence patterns
void process(String config) {
if (config.equals("special")) {
Class.forName("SpecialClass"); // ← hardcoded literal
}
Class.forName(config); // ← parameter reference
}
Classification: Two separate influence entries
- First call →
control_flow (parameter selects whether this literal-based call happens)
- Second call →
direct (parameter value flows to API)
Key distinction
The critical distinction is between:
Class.forName("Foo") — hardcoded literal → control_flow if parameter selects this branch
Class.forName(paramName) — parameter reference → direct even if parameter appears in control flow conditions
When a parameter appears in both a condition and as the API operand (e.g., if (x.equals("A")) Class.forName(x)), classify as direct because the operand is the parameter itself — not a literal that happens to match.
When a parameter appears in a condition but the API uses literals (e.g., if (x.equals("A")) Class.forName("A")), classify as control_flow because the operand is a literal — the parameter merely selects which literal executes.
Composition of dependency kinds
When a parameter's influence flows through multiple steps (e.g., parameter → intermediate value → tracked API), the combined dependency kind follows a join semilattice with total order:
direct < derived < control_flow
The composition rule is: take the maximum (least direct) of the two kinds.
Composition table:
| First step |
Second step |
Combined result |
| direct |
direct |
direct |
| direct |
derived |
derived |
| direct |
control_flow |
control_flow |
| derived |
direct |
derived |
| derived |
derived |
derived |
| derived |
control_flow |
control_flow |
| control_flow |
direct |
control_flow |
| control_flow |
derived |
control_flow |
| control_flow |
control_flow |
control_flow |
Lattice diagram:
control_flow ← top (least direct: parameter only selects path)
│
derived ← middle (parameter transformed before reaching operand)
│
direct ← bottom (most direct: parameter IS the operand)
Intuition:
direct means value identity is preserved through the entire chain
derived means value is transformed at some point, but still flows to the operand
control_flow means at some point the value stops flowing and instead selects a path; whatever happens downstream, the original parameter only controlled execution
Composition examples:
-
direct ⊕ direct = direct
String alias = param; // direct: param → alias
Class.forName(alias); // direct: alias → API
// Combined: param → API is direct
-
direct ⊕ derived = derived
String name = param; // direct: param → name
Class.forName(name + "Impl"); // derived: name → API
// Combined: param → API is derived
-
derived ⊕ direct = derived
String upper = param.toUpperCase(); // derived: param → upper
Class.forName(upper); // direct: upper → API
// Combined: param → API is derived
-
direct ⊕ control_flow = control_flow
String mode = param; // direct: param → mode
if (mode.equals("A")) Class.forName("X"); // control_flow: mode → API
// Combined: param → API is control_flow
-
control_flow ⊕ direct = control_flow
// param selects which value to use (control_flow)
String target = param.equals("A") ? "ClassA" : "ClassB";
Class.forName(target); // direct: target → API
// Combined: param → API is control_flow (param selected, didn't flow)
Metadata repository contract
- Define a new root-level repository data location, outside
metadata/<group>/<artifact>/<version>/, so it is not copied into META-INF/native-image:
exposed-dynamic-access/<groupId>/<artifactId>/<metadataVersion>/exposed-dynamic-access.json
- Do not store this file beside
reachability-metadata.json; existing copy logic would treat it as runtime config.
- Native Build Tools reuses the existing reachability metadata version resolution logic—no new resolution implementation needed. The same
metadataVersion resolved for reachability metadata applies here.
Shared core behavior
- Put the shared model, parser, merger, and deterministic serializer in shared
common/ code so Gradle and Maven wrappers stay thin.
- The shared core takes:
- Repository root
- Resolved classpath artifacts for the current build scope
- Effective metadata version per artifact (obtained from existing reachability metadata resolution)
- The shared core returns methods grouped by class, aggregated from all artifacts that have parameter-influence data.
- No fallback local analysis for missing library metadata. If repo data is absent for an artifact, skip that artifact. (Local source analysis in Step 3 is additive, not a fallback.)
- Write deterministic output ordering:
- Inputs by
artifact
- Classes by
className
- Methods by
signature
- Parameters by ascending index
- Influences by
dependencyKind, dynamicAccessType, trackedApi
Schema validation (shared core)
Native Build Tools performs lightweight schema validation on consumed exposed-dynamic-access.json:
| Condition |
Action |
| Invalid JSON structure |
Skip artifact with warning |
Unknown dependencyKind value |
Skip entry with warning |
| Missing required fields |
Skip entry with warning |
Bytecode-level validation (class exists, method exists, parameter count) is performed by agent-based analysis in the metadata repository pipeline, not in Native Build Tools.
File structure in Native Build Tools
native-build-tools/
├── common/
│ └── src/main/java/org/graalvm/buildtools/
│ └── dap/ # dynamic-access-parameters
│ ├── model/
│ │ ├── DynamicAccessModels.java # Shared data classes
│ │ ├── CallSite.java # Call site correlation model
│ │ └── ArgumentBinding.java # Argument binding model
│ ├── DynamicAccessParameterDependenciesParser.java
│ ├── DynamicAccessParameterDependenciesMerger.java
│ ├── DynamicAccessParameterDependenciesSerializer.java
│ └── analysis/
│ ├── SourceAnalysisAgent.java # Agent interface
│ ├── SourceAnalysisConfig.java # Configuration model
│ ├── SourceAnalysisResult.java # Analysis output model
│ ├── CorrelationAnalyzer.java # Finds call sites to exposed methods
│ ├── IntrinsicDetector.java # Finds direct dynamic access in user code
│ └── TrackedApis.java # Tracked API definitions
├── native-gradle-plugin/
│ └── src/main/java/org/graalvm/buildtools/gradle/
│ ├── tasks/
│ │ └── GenerateDynamicAccessParameterDependenciesTask.java
│ └── dsl/
│ └── SourceAnalysisOptions.java # Gradle DSL extension
└── native-maven-plugin/
└── src/main/java/org/graalvm/buildtools/maven/
└── GenerateDynamicAccessParameterDependenciesMojo.java
Input from metadata repository
The metadata repository publishes per-artifact method data at:
exposed-dynamic-access/<groupId>/<artifactId>/<metadataVersion>/exposed-dynamic-access.json
Native Build Tools reads these files for each classpath artifact, reusing the metadata-version already resolved by the existing reachability metadata logic.
Missing exposed-dynamic-access.json files are not errors; artifacts without parameter influence data are simply omitted from the output.
Agent-based source analysis (Step 3)
Step 3 performs optional neural/agent-based analysis of user source code to enrich repository data with local context. This step is optional and additive—it does not replace or fallback for missing repository data.
Purpose
Repository data answers: "Which library methods expose parameters to dynamic access?"
Local analysis answers:
- Correlation: "Where does user code call those exposed methods, and with what arguments?"
- Intrinsic: "What direct dynamic access calls exist in user code itself?"
Together, these provide a complete picture of dynamic access patterns across the entire application.
Analysis modes
| Mode |
Input |
Detects |
Output |
| Correlation |
User bytecode/source + fetched exposures |
Call sites invoking exposed library methods |
callSites per exposed method |
| Intrinsic |
User bytecode/source |
Direct calls to tracked APIs in user code |
dynamicAccessExposures (same schema as library exposures) |
Correlation analysis
Finds locations in user code that invoke methods from exposed-dynamic-access.json:
{
"callSites": [
{
"exposedMethod": {
"className": "com.example.Library",
"signature": "load(java.lang.String, boolean) -> java.lang.Class"
},
"callerLocation": {
"className": "com.myapp.Main",
"methodSignature": "initialize(java.lang.String) -> void",
"lineNumber": 42
},
"argumentBindings": [
{
"parameterIndex": 0,
"bindingKind": "parameter_passthrough",
"sourceParameter": {
"index": 0,
"name": "configName"
}
},
{
"parameterIndex": 1,
"bindingKind": "constant",
"constantValue": "true"
}
]
}
]
}
Argument binding kinds:
bindingKind |
Meaning |
Implication |
parameter_passthrough |
Caller's parameter flows directly to exposed method |
Propagates exposure up the call chain |
parameter_derived |
Value computed from caller's parameter |
Propagates exposure with derived kind |
constant |
Literal/compile-time constant |
Statically resolvable, no exposure propagation |
field_reference |
Instance or static field |
May require field tracking |
method_return |
Return value from another method call |
May require interprocedural analysis |
unknown |
Could not determine binding |
Conservative: assume dynamic |
Intrinsic dynamic access detection
Finds direct calls to tracked APIs within user code and applies the same classification rules as library analysis:
{
"dynamicAccessExposures": [
{
"className": "com.myapp.PluginLoader",
"methods": [
{
"signature": "loadPlugin(java.lang.String) -> java.lang.Object",
"parameters": [
{
"index": 0,
"influences": [
{
"dependencyKind": "direct",
"dynamicAccessType": "reflection",
"trackedApi": "java.lang.Class#forName(java.lang.String)",
"location": {
"lineNumber": 15
}
}
]
}
]
}
]
}
]
}
The schema matches library exposures but adds location.lineNumber for user code (since source is available).
Tracked APIs
The agent detects calls to these dynamic access APIs:
| Category |
Tracked APIs |
| Reflection |
Class.forName, Class.getMethod, Class.getDeclaredMethod, Class.getField, Class.getDeclaredField, Class.getConstructor, Class.getDeclaredConstructor, Class.newInstance, Constructor.newInstance, Method.invoke, Field.get, Field.set |
| Resources |
Class.getResource, Class.getResourceAsStream, ClassLoader.getResource, ClassLoader.getResourceAsStream |
| Proxies |
Proxy.newProxyInstance, Proxy.getProxyClass |
| Serialization |
ObjectInputStream.readObject, ObjectOutputStream.writeObject |
| JNI |
System.loadLibrary, System.load, Runtime.loadLibrary, Runtime.load |
Agent implementation options
The analysis can be performed by:
- LLM-based agent: Analyzes source code with language model, suitable for complex dataflow patterns
- Static analysis agent: Traditional dataflow analysis (e.g., Soot, WALA, SpotBugs plugin)
- Hybrid: LLM for classification decisions, static analysis for call graph extraction
The agent interface is:
Input:
- User source/bytecode root paths
- Fetched exposed-dynamic-access.json files (from step 2)
- Configuration (analysis depth, timeout, tracked APIs)
Output:
- callSites: correlation data
- dynamicAccessExposures: intrinsic dynamic access in user code
Configuration
// Gradle
graalvmNative {
agent {
sourceAnalysis {
enabled = true // default: false
mode = "correlation+intrinsic" // or "correlation" or "intrinsic"
sourcePaths = [sourceSets.main.java.srcDirs]
maxAnalysisDepth = 5 // call chain depth limit
timeoutSeconds = 300
}
}
}
<!-- Maven -->
<configuration>
<sourceAnalysis>
<enabled>true</enabled>
<mode>correlation+intrinsic</mode>
<maxAnalysisDepth>5</maxAnalysisDepth>
<timeoutSeconds>300</timeoutSeconds>
</sourceAnalysis>
</configuration>
Integration with merge step
When source analysis is enabled, step 4 (merge) combines:
- Library exposures (from repository, step 2)
- User code exposures (from intrinsic detection as
dynamicAccessExposures, step 3)
- Call site data (from correlation analysis, step 3)
The merged output distinguishes sources via origin field:
{
"classes": [
{
"className": "com.example.Library",
"origin": "repository",
"methods": [...]
},
{
"className": "com.myapp.PluginLoader",
"origin": "source_analysis",
"methods": [...]
}
],
"callSites": [...]
}
Plugin wiring
- Gradle:
- Register the task per binary, using that binary's runtime classpath
- Do not auto-attach it to
nativeCompile, nativeTest, or build-report classpath wiring
- Maven:
- Resolve against the project runtime artifacts
- Keep it as an explicit goal, not an automatic lifecycle side effect beyond its own invocation
- Reuse the existing metadata repository configuration knobs in both plugins; do not introduce a second repository config path.
Test Plan
Shared-core unit tests
- Parses a repo
exposed-dynamic-access.json file with direct, derived, and control_flow influences
- Merges methods from multiple artifacts, grouping by class with deterministic ordering
- Omits classes/methods with no influenced parameters
- Emits
repositoryAvailable: false with empty classes array when repo root is unavailable
Schema validation tests
| Test case |
Expected |
| Valid JSON with all required fields |
Include in output |
| Invalid JSON structure |
Skip artifact with warning |
Unknown dependencyKind value |
Skip entry with warning |
| Missing required fields |
Skip entry with warning |
Version-resolution tests
- Verify that version resolution delegates to existing reachability metadata logic
- Artifact present on classpath but missing sidecar JSON is skipped
Source analysis unit tests (Step 3)
Correlation analysis tests
| Test case |
Expected |
| Call to exposed method with parameter passthrough |
bindingKind: parameter_passthrough with source parameter info |
| Call to exposed method with constant argument |
bindingKind: constant with constant value |
| Call to exposed method with derived value |
bindingKind: parameter_derived |
| Call to exposed method with field reference |
bindingKind: field_reference |
| Call to exposed method with method return value |
bindingKind: method_return |
| Unresolvable argument expression |
bindingKind: unknown |
| No calls to exposed methods in user code |
Empty callSites array |
Intrinsic detection tests
| Test case |
Expected |
Direct Class.forName(param) |
direct influence on parameter |
Class.forName(prefix + param) |
derived influence on parameter |
if (flag) Class.forName("Literal") |
control_flow influence on flag parameter |
| Nested call chain to tracked API |
Correct composed dependency kind |
| No dynamic access in user code |
Empty userCodeExposures |
Integration tests
- Correlation + intrinsic combined produces merged output with both origins
- Source analysis disabled produces output with empty
callSites array and no source_analysis origin entries
- Analysis timeout produces partial results with warning
- Missing source paths produces empty source analysis with warning
Gradle functional tests
- Main binary task aggregates methods from current runtime-classpath artifacts with repo data
- Test binary task uses test runtime classpath, not main-only artifacts
- Task is standalone and not injected into native-image classpath/build-report flow
- Source analysis enabled: task analyzes configured source paths
- Source analysis disabled: task skips step 3, outputs repo data only
Maven functional tests
- Goal emits expected JSON aggregated from runtime artifacts
- Honors configured metadata repository version/URI
- Produces empty-but-valid output when repository data is unavailable
- Source analysis configuration honored when enabled
Acceptance scenarios
- One parameter with multiple influence records
- Multiple parameters in one method
- Same tracked API reached through different dependency kinds
- Stable output across repeated runs
- User code calls library exposed method with parameter passthrough → transitive exposure detected
- User code has both library calls and direct dynamic access → both appear in output
Assumptions
- The metadata repository will publish the new per-artifact method JSON under
exposed-dynamic-access/<groupId>/<artifactId>/<metadataVersion>/exposed-dynamic-access.json.
- That repo JSON is produced by agent-based static analysis of library source code in the metadata repository pipeline. The agent analyzes source code to identify methods whose parameters influence dynamic access calls and classifies the dependency kind (
direct, derived, control_flow).
- Bytecode-level validation (class exists, method signature matches, parameter count correct) is performed by the agent during metadata generation, not by Native Build Tools at consumption time.
- For library artifacts: Native Build Tools consumes pre-analyzed repository data without re-analysis.
- For user code (step 3): Native Build Tools performs local source analysis when enabled, using the same classification rules as library analysis.
- Source analysis (step 3) is optional and additive—it enriches but does not replace repository data.
- The source analysis agent interface is abstract; initial implementation may use static analysis (bytecode/AST), with LLM-based analysis as a future enhancement.
- Analysis depth and timeout are configurable to balance precision vs. build time.
- Version resolution for metadata lookup reuses the existing reachability metadata resolution logic—no new resolution mechanism is needed.
- The first consumer is standalone JSON only. No build report UI integration is included in this change.
Summary
Add a new repo-driven analysis output to Native Build Tools that emits, for the current resolved classpath, only the methods whose parameters participate in dynamic-access calls.
The task/goal reads precomputed method-level JSON from the configured reachability metadata repository, resolves the effective metadata version for each classpath artifact using the same repository-selection rules already used for reachability metadata, filters to the current project classpath, and writes a standalone JSON artifact.
The method-level JSON in the repository is produced by agent-based static analysis of library source code in the metadata repository pipeline. Native Build Tools consumes this pre-validated data without re-analyzing bytecode.
Native Build Tools performs local agent-based analysis of user source code to:
This provides a complete picture: library exposures from the repository + user code exposures and call sites from local analysis.
Workflow Diagram
Key Changes
Public interfaces
generateExposedDynamicAccessgenerateExposedDynamicAccessbuild/generated/<task-name>/exposed-dynamic-access.jsontarget/exposed-dynamic-access.jsonOutput JSON
The task produces a single aggregated output for the entire classpath, with methods grouped by class and merged from all artifacts that have parameter influence data:
{ "schemaVersion": "1.0.0", "repositoryAvailable": true, "inputs": [ { "artifact": "com.example:library:1.2.3", "metadataVersion": "1.2.0" }, { "artifact": "org.other:util:4.5.6", "metadataVersion": "4.5.0" } ], "classes": [ { "className": "com.example.Library", "origin": "repository", "methods": [ { "signature": "load(java.lang.String, boolean) -> java.lang.Class", "parameters": [ { "index": 0, "influences": [ { "dependencyKind": "direct", "dynamicAccessType": "reflection", "trackedApi": "java.lang.Class#forName(java.lang.String)" }, { "dependencyKind": "derived", "dynamicAccessType": "reflection", "trackedApi": "java.lang.Class#getDeclaredMethod(java.lang.String,java.lang.Class[])" } ] }, { "index": 1, "influences": [ { "dependencyKind": "control_flow", "dynamicAccessType": "reflection", "trackedApi": "java.lang.Class#forName(java.lang.String)" } ] } ] } ] } ], "callSites": [] }Rules:
inputslists only artifacts that contributed parameter influence data (hadexposed-dynamic-access.jsonin repo).inputs[*].artifactisgroupId:artifactId:versionfrom the classpath.inputs[*].metadataVersionis the resolved metadata version used to readexposed-dynamic-access.json.classes[*].originis"repository"for library exposures from the metadata repository, or"source_analysis"for exposures detected in user code by local analysis.callSitescontains correlation data from source analysis (empty array when source analysis is disabled).dependencyKindis a closed set. SeeDependency kind semanticsbelow.dynamicAccessTypeis pass-through from repo data; do not hard-code it beyond string transport.repositoryAvailable: false,inputs: [],classes: [], andcallSites: [].Dependency kind semantics
dependencyKindmust be one ofdirect,derived, orcontrol_flow. Native Build Tools treats this as repo-authored classification data and must reject or ignore any unexpected value rather than inventing a fallback category.directmeans the parameter value itself is used as the dynamic-access input, or it flows to that input without any material transformation. Typical cases are passing aStringparameter directly toClass.forName, using a parameter as the member name in reflective lookup, or forwarding the same value through simple aliases.derivedmeans the parameter still influences the dynamic-access input, but only after an intermediate transformation, lookup, extraction, or composition step. Typical cases are deriving a class name from a parameter object, reading a reflective target from a map keyed by the parameter, or concatenating a package prefix with the parameter before the reflective call.control_flowmeans the parameter does not supply the dynamic-access operand itself, but it decides whether a dynamic-access call happens or which precomputed target is chosen. Typical cases are boolean flags, enums, or mode strings that select between branches where each branch performs different reflection/resource/proxy access.directwhen the tracked API consumes the parameter value itself.derivedwhen the tracked API consumes a value computed from the parameter.control_flowwhen the parameter only selects the path that eventually performs dynamic access.Classification decision rule
The classification depends on data flow to the API operand, not on whether the parameter appears in control flow constructs. The key question is: does the parameter's VALUE flow to the tracked API, or does the parameter merely SELECT which predetermined value gets used?
className)direct"com.example.Foo")control_flowname + ".Impl")derivedExamples
Example 1: Pure direct — no control flow
Classification:
direct— the parameter IS the operand.Example 2: Direct with validation guard
Classification:
direct— despite the conditional guard, the parameter value itself flows toClass.forName. The control flow is validation/filtering, not target selection.Example 3: Parameter in switch, passed directly in each case
Classification:
direct— the switch constrains possible values, but the parameter itself flows to the API. The operand istype, not a literal.Example 4: Control flow selecting hardcoded targets
Classification:
control_flow— the parameter selects the branch, but the reflection targets are hardcoded literals. Native Image can statically enumerate both targets.Example 5: Condition values match literals (no data flow)
Classification:
control_flow— even though the literals coincidentally match the condition values, there is no data flow from parameter to operand. The operands are string literals. The parameter merely selects which literal gets used.Example 6: Boolean/enum parameter
Classification:
control_flow— the parameter cannot possibly be the class name (wrong type). It purely selects which hardcoded target is used.Example 7: Derived with control flow guard
Classification:
derived— the operand is computed from the parameter via string concatenation, not the parameter itself.Example 8: Same parameter, multiple influence patterns
Classification: Two separate influence entries
control_flow(parameter selects whether this literal-based call happens)direct(parameter value flows to API)Key distinction
The critical distinction is between:
Class.forName("Foo")— hardcoded literal →control_flowif parameter selects this branchClass.forName(paramName)— parameter reference →directeven if parameter appears in control flow conditionsWhen a parameter appears in both a condition and as the API operand (e.g.,
if (x.equals("A")) Class.forName(x)), classify asdirectbecause the operand is the parameter itself — not a literal that happens to match.When a parameter appears in a condition but the API uses literals (e.g.,
if (x.equals("A")) Class.forName("A")), classify ascontrol_flowbecause the operand is a literal — the parameter merely selects which literal executes.Composition of dependency kinds
When a parameter's influence flows through multiple steps (e.g., parameter → intermediate value → tracked API), the combined dependency kind follows a join semilattice with total order:
The composition rule is: take the maximum (least direct) of the two kinds.
Composition table:
Lattice diagram:
Intuition:
directmeans value identity is preserved through the entire chainderivedmeans value is transformed at some point, but still flows to the operandcontrol_flowmeans at some point the value stops flowing and instead selects a path; whatever happens downstream, the original parameter only controlled executionComposition examples:
direct ⊕ direct = direct
direct ⊕ derived = derived
derived ⊕ direct = derived
direct ⊕ control_flow = control_flow
control_flow ⊕ direct = control_flow
Metadata repository contract
metadata/<group>/<artifact>/<version>/, so it is not copied intoMETA-INF/native-image:exposed-dynamic-access/<groupId>/<artifactId>/<metadataVersion>/exposed-dynamic-access.jsonreachability-metadata.json; existing copy logic would treat it as runtime config.metadataVersionresolved for reachability metadata applies here.Shared core behavior
common/code so Gradle and Maven wrappers stay thin.artifactclassNamesignaturedependencyKind,dynamicAccessType,trackedApiSchema validation (shared core)
Native Build Tools performs lightweight schema validation on consumed
exposed-dynamic-access.json:dependencyKindvalueBytecode-level validation (class exists, method exists, parameter count) is performed by agent-based analysis in the metadata repository pipeline, not in Native Build Tools.
File structure in Native Build Tools
Input from metadata repository
The metadata repository publishes per-artifact method data at:
Native Build Tools reads these files for each classpath artifact, reusing the metadata-version already resolved by the existing reachability metadata logic.
Missing
exposed-dynamic-access.jsonfiles are not errors; artifacts without parameter influence data are simply omitted from the output.Agent-based source analysis (Step 3)
Step 3 performs optional neural/agent-based analysis of user source code to enrich repository data with local context. This step is optional and additive—it does not replace or fallback for missing repository data.
Purpose
Repository data answers: "Which library methods expose parameters to dynamic access?"
Local analysis answers:
Together, these provide a complete picture of dynamic access patterns across the entire application.
Analysis modes
callSitesper exposed methoddynamicAccessExposures(same schema as library exposures)Correlation analysis
Finds locations in user code that invoke methods from
exposed-dynamic-access.json:{ "callSites": [ { "exposedMethod": { "className": "com.example.Library", "signature": "load(java.lang.String, boolean) -> java.lang.Class" }, "callerLocation": { "className": "com.myapp.Main", "methodSignature": "initialize(java.lang.String) -> void", "lineNumber": 42 }, "argumentBindings": [ { "parameterIndex": 0, "bindingKind": "parameter_passthrough", "sourceParameter": { "index": 0, "name": "configName" } }, { "parameterIndex": 1, "bindingKind": "constant", "constantValue": "true" } ] } ] }Argument binding kinds:
bindingKindparameter_passthroughparameter_derivedderivedkindconstantfield_referencemethod_returnunknownIntrinsic dynamic access detection
Finds direct calls to tracked APIs within user code and applies the same classification rules as library analysis:
{ "dynamicAccessExposures": [ { "className": "com.myapp.PluginLoader", "methods": [ { "signature": "loadPlugin(java.lang.String) -> java.lang.Object", "parameters": [ { "index": 0, "influences": [ { "dependencyKind": "direct", "dynamicAccessType": "reflection", "trackedApi": "java.lang.Class#forName(java.lang.String)", "location": { "lineNumber": 15 } } ] } ] } ] } ] }The schema matches library exposures but adds
location.lineNumberfor user code (since source is available).Tracked APIs
The agent detects calls to these dynamic access APIs:
Class.forName,Class.getMethod,Class.getDeclaredMethod,Class.getField,Class.getDeclaredField,Class.getConstructor,Class.getDeclaredConstructor,Class.newInstance,Constructor.newInstance,Method.invoke,Field.get,Field.setClass.getResource,Class.getResourceAsStream,ClassLoader.getResource,ClassLoader.getResourceAsStreamProxy.newProxyInstance,Proxy.getProxyClassObjectInputStream.readObject,ObjectOutputStream.writeObjectSystem.loadLibrary,System.load,Runtime.loadLibrary,Runtime.loadAgent implementation options
The analysis can be performed by:
The agent interface is:
Configuration
Integration with merge step
When source analysis is enabled, step 4 (merge) combines:
dynamicAccessExposures, step 3)The merged output distinguishes sources via
originfield:{ "classes": [ { "className": "com.example.Library", "origin": "repository", "methods": [...] }, { "className": "com.myapp.PluginLoader", "origin": "source_analysis", "methods": [...] } ], "callSites": [...] }Plugin wiring
nativeCompile,nativeTest, or build-report classpath wiringTest Plan
Shared-core unit tests
exposed-dynamic-access.jsonfile withdirect,derived, andcontrol_flowinfluencesrepositoryAvailable: falsewith emptyclassesarray when repo root is unavailableSchema validation tests
dependencyKindvalueVersion-resolution tests
Source analysis unit tests (Step 3)
Correlation analysis tests
bindingKind: parameter_passthroughwith source parameter infobindingKind: constantwith constant valuebindingKind: parameter_derivedbindingKind: field_referencebindingKind: method_returnbindingKind: unknowncallSitesarrayIntrinsic detection tests
Class.forName(param)directinfluence on parameterClass.forName(prefix + param)derivedinfluence on parameterif (flag) Class.forName("Literal")control_flowinfluence on flag parameteruserCodeExposuresIntegration tests
callSitesarray and nosource_analysisorigin entriesGradle functional tests
Maven functional tests
Acceptance scenarios
Assumptions
exposed-dynamic-access/<groupId>/<artifactId>/<metadataVersion>/exposed-dynamic-access.json.direct,derived,control_flow).