Compare commits
1 Commits
613dfffd53
...
ai-branch-
| Author | SHA1 | Date | |
|---|---|---|---|
| bb97285906 |
45
README.md
45
README.md
@@ -8,46 +8,16 @@ Static analysis tool to extract and visualize Spring State Machines with deep co
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Analysis is **source-first**: the exporter reads project source and build metadata (`pom.xml`, `settings.gradle`) statically. It does **not** run Maven or Gradle against the project being analyzed.
|
|
||||||
|
|
||||||
### 1. Extract Metadata (JSON)
|
### 1. Extract Metadata (JSON)
|
||||||
```bash
|
```bash
|
||||||
./gradlew :state_machine_exporter:run --args="-i ./my-project -o ./out -f json"
|
./gradlew :state_machine_exporter:run --args="-i ./my-project -o ./out -f json"
|
||||||
```
|
```
|
||||||
|
|
||||||
Common options:
|
|
||||||
|
|
||||||
| Flag | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `-i, --input` | Project directory to scan (defaults to `.`) |
|
|
||||||
| `-j, --json` | Re-render from an existing JSON export instead of scanning source |
|
|
||||||
| `-o, --output` | Output directory (default `./out`) |
|
|
||||||
| `-f, --format` | Comma-separated formats: `json`, `dot`, `puml`, `scxml` |
|
|
||||||
| `-p, --profiles` | Spring profiles for property placeholder resolution |
|
|
||||||
| `--event`, `--state` | Enum formatting: `fn` (default), `fqn`, `sn` |
|
|
||||||
| `--debug` | Verbose analysis logging and unresolved chain diagnostics |
|
|
||||||
| `--no-diamonds` | Render choice/junction states as regular nodes (default: diamonds) |
|
|
||||||
| `--include-generated-sources` | Also scan `target/` / `build/` generated `.java` trees (default: on) |
|
|
||||||
| `--no-inline-accessors` | Disable accessor indexing/inlining (enabled by default) |
|
|
||||||
|
|
||||||
### 2. Generate Interactive Portal (HTML)
|
### 2. Generate Interactive Portal (HTML)
|
||||||
```bash
|
```bash
|
||||||
./gradlew :state_machine_exporter_html:run --args="-i ./my-project -o ./out_html"
|
./gradlew :state_machine_exporter_html:run --args="-i ./my-project -o ./out_html"
|
||||||
```
|
```
|
||||||
|
*Supports `--profiles prod` to resolve Spring property placeholders.*
|
||||||
HTML-specific options:
|
|
||||||
|
|
||||||
| Flag | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `-f, --flows` | Custom `flows.json` path |
|
|
||||||
| `-m, --machine` | Export only machines whose name contains this filter |
|
|
||||||
| `--open` | Open the generated portal in the default browser |
|
|
||||||
| `--no-metadata-pane` | Hide the left metadata pane (entry points, flows) |
|
|
||||||
| `--no-diamonds` | Render choice pseudostates as normal nodes |
|
|
||||||
| `--debug` | Same diagnostic mode as the core exporter |
|
|
||||||
| `--include-generated-sources` | Same generated-source scanning as the core exporter |
|
|
||||||
| `--no-inline-accessors` | Disable accessor indexing/inlining (enabled by default) |
|
|
||||||
| `-p, --profiles` | Spring profiles for property resolution |
|
|
||||||
|
|
||||||
## Business Flows
|
## Business Flows
|
||||||
Define sequence of events in `src/main/resources/flows.json`:
|
Define sequence of events in `src/main/resources/flows.json`:
|
||||||
@@ -66,16 +36,3 @@ Define sequence of events in `src/main/resources/flows.json`:
|
|||||||
- `metadata.callChains`: Trace from API call to machine trigger (`sendEvent`).
|
- `metadata.callChains`: Trace from API call to machine trigger (`sendEvent`).
|
||||||
- `transitions.actions[].internalLogic`: Extracted source code/lambdas.
|
- `transitions.actions[].internalLogic`: Extracted source code/lambdas.
|
||||||
- `metadata.properties`: Dictionary of all detected Spring profiles.
|
- `metadata.properties`: Dictionary of all detected Spring profiles.
|
||||||
|
|
||||||
## Static analysis: accessor inlining
|
|
||||||
|
|
||||||
During scan, the exporter builds an **accessor index** of trivial getters, setters, record components, and constant-returning methods. At analysis time, `AccessorResolver` uses this index first (O(1) lookup), then falls back to constructor/field tracing and method-body evaluation.
|
|
||||||
|
|
||||||
This improves resolution of DTO/event chains such as `request.getPayload().getType()` without running Maven or Gradle on the target project.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Enabled by default; disable for debugging or A/B comparison
|
|
||||||
./gradlew :state_machine_exporter:run --args="-i ./my-project -o ./out --no-inline-accessors"
|
|
||||||
```
|
|
||||||
|
|
||||||
Pipeline components live under `analysis/pipeline/` (`AccessorResolver`, `ResolutionBudget`, `MethodInvocationUnwrapper`, `FieldInitializerFinder`, `LibraryUnwrapRegistry`).
|
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ include ':state_machines:inheritance_extra_functions3_state_machine'
|
|||||||
include ':state_machines:simple_state_machine'
|
include ':state_machines:simple_state_machine'
|
||||||
include ':state_machines:extended_analysis_sample'
|
include ':state_machines:extended_analysis_sample'
|
||||||
include ':state_machines:inheritance_sample'
|
include ':state_machines:inheritance_sample'
|
||||||
include ':state_machines:layered_dispatcher_sample'
|
|
||||||
include ':state_machines:ultimate_ecosystem_sm'
|
include ':state_machines:ultimate_ecosystem_sm'
|
||||||
include ':state_machines:enterprise_order_system'
|
include ':state_machines:enterprise_order_system'
|
||||||
include ':state_machines:multi_module_sample:api-module'
|
include ':state_machines:multi_module_sample:api-module'
|
||||||
include ':state_machines:multi_module_sample:core-module'
|
include ':state_machines:multi_module_sample:core-module'
|
||||||
include ':state_machines:state_machine_enterprise'
|
include ':state_machines:state_machine_enterprise'
|
||||||
|
include 'state_machine_exporter_kotlin'
|
||||||
|
include ':state_machines:kotlin_simple_state_machine'
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ dependencies {
|
|||||||
// deps for parsing java AST
|
// deps for parsing java AST
|
||||||
implementation 'org.eclipse.jdt:org.eclipse.jdt.core:3.36.0'
|
implementation 'org.eclipse.jdt:org.eclipse.jdt.core:3.36.0'
|
||||||
|
|
||||||
|
// Kotlin plugin integration via ServiceLoader
|
||||||
|
runtimeOnly project(':state_machine_exporter_kotlin')
|
||||||
|
|
||||||
// Jackson for JSON support
|
// Jackson for JSON support
|
||||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
|
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
|
||||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.17.1'
|
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.17.1'
|
||||||
@@ -48,15 +51,10 @@ dependencies {
|
|||||||
testImplementation 'org.assertj:assertj-core:3.27.7'
|
testImplementation 'org.assertj:assertj-core:3.27.7'
|
||||||
|
|
||||||
implementation 'net.sourceforge.plantuml:plantuml:1.2024.3'
|
implementation 'net.sourceforge.plantuml:plantuml:1.2024.3'
|
||||||
|
|
||||||
// Library types for JDT binding when analyzing cloned source (never runs the target project's build).
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter:3.2.0'
|
|
||||||
implementation 'org.springframework.statemachine:spring-statemachine-starter:3.2.0'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('test') {
|
tasks.named('test') {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
maxParallelForks = Math.max(1, Runtime.runtime.availableProcessors().intdiv(2) ?: 1)
|
|
||||||
systemProperty "updateGolden", System.getProperty("updateGolden")
|
systemProperty "updateGolden", System.getProperty("updateGolden")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Deep audit of golden JSON analysis exports."""
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
from collections import defaultdict
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
GOLDEN = Path(__file__).resolve().parent.parent / "src/test/resources/golden"
|
|
||||||
PH = re.compile(r"\$\{")
|
|
||||||
|
|
||||||
|
|
||||||
def fn_form(identifier):
|
|
||||||
if not identifier:
|
|
||||||
return identifier
|
|
||||||
last = identifier.rfind(".")
|
|
||||||
if last <= 0:
|
|
||||||
return identifier
|
|
||||||
prev = identifier.rfind(".", 0, last)
|
|
||||||
return identifier[prev + 1 :] if prev > 0 else identifier
|
|
||||||
|
|
||||||
|
|
||||||
def sid(st):
|
|
||||||
if isinstance(st, dict):
|
|
||||||
return st.get("fullIdentifier") or st.get("rawName")
|
|
||||||
return st
|
|
||||||
|
|
||||||
|
|
||||||
def eid(ev):
|
|
||||||
if isinstance(ev, dict):
|
|
||||||
return ev.get("fullIdentifier") or ev.get("rawName")
|
|
||||||
return ev
|
|
||||||
|
|
||||||
|
|
||||||
def audit_file(path, data):
|
|
||||||
name = data.get("name", path.stem)
|
|
||||||
issues = []
|
|
||||||
st_fqn = data.get("stateTypeFqn")
|
|
||||||
ev_fqn = data.get("eventTypeFqn")
|
|
||||||
transitions = data.get("transitions") or []
|
|
||||||
starts = list(data.get("startStates") or [])
|
|
||||||
ends = list(data.get("endStates") or [])
|
|
||||||
states = data.get("states") or []
|
|
||||||
meta = data.get("metadata") or {}
|
|
||||||
|
|
||||||
is_pkg_enum = st_fqn and "." in st_fqn and st_fqn not in ("String", "java.lang.String")
|
|
||||||
is_string = st_fqn in (None, "String", "java.lang.String") or (
|
|
||||||
not st_fqn and not ev_fqn
|
|
||||||
)
|
|
||||||
|
|
||||||
if PH.search(json.dumps(data)):
|
|
||||||
issues.append("BUG: unresolved ${placeholder} in export")
|
|
||||||
|
|
||||||
# Duplicate fullIdentifiers in states[]
|
|
||||||
by_full = defaultdict(list)
|
|
||||||
for s in states:
|
|
||||||
by_full[sid(s)].append(s.get("rawName"))
|
|
||||||
for fid, raws in by_full.items():
|
|
||||||
if fid and len(raws) > 1 and len(set(raws)) > 1:
|
|
||||||
issues.append(f"NOISE: duplicate state entry {fid!r} with rawNames {raws}")
|
|
||||||
|
|
||||||
# Collect transition state/event ids
|
|
||||||
t_events, t_sources, t_targets = set(), set(), set()
|
|
||||||
for t in transitions:
|
|
||||||
if eid(t.get("event")):
|
|
||||||
t_events.add(eid(t.get("event")))
|
|
||||||
for s in t.get("sourceStates") or []:
|
|
||||||
t_sources.add(sid(s))
|
|
||||||
for s in t.get("targetStates") or []:
|
|
||||||
t_targets.add(sid(s))
|
|
||||||
|
|
||||||
graph = t_sources | t_targets
|
|
||||||
|
|
||||||
# startStates canonical consistency
|
|
||||||
for ss in starts:
|
|
||||||
if is_string and ss and not ss.startswith("String."):
|
|
||||||
if any(x.startswith("String.") for x in graph):
|
|
||||||
issues.append(f"BUG: startStates {ss!r} bare but transitions use String.* form")
|
|
||||||
if ss not in graph and fn_form(ss) not in {fn_form(x) for x in graph}:
|
|
||||||
issues.append(f"WARN: startStates {ss!r} not in transition graph")
|
|
||||||
|
|
||||||
# endStates
|
|
||||||
for es in ends:
|
|
||||||
if es not in graph and fn_form(es) not in {fn_form(x) for x in graph}:
|
|
||||||
issues.append(f"WARN: endStates {es!r} not in transition graph")
|
|
||||||
|
|
||||||
# Enum canonical fullIdentifiers
|
|
||||||
if is_pkg_enum:
|
|
||||||
for i, t in enumerate(transitions):
|
|
||||||
for side in ("sourceStates", "targetStates"):
|
|
||||||
for s in t.get(side) or []:
|
|
||||||
fid = sid(s)
|
|
||||||
if fid and not fid.startswith(st_fqn + ".") and not fid.startswith('"'):
|
|
||||||
if "." in fid and not fid.startswith("<"):
|
|
||||||
issues.append(f"BUG: transitions[{i}].{side} {fid!r} not under {st_fqn}")
|
|
||||||
ev = eid(t.get("event"))
|
|
||||||
if ev and ev_fqn and "." in ev_fqn and not ev.startswith("<") and not ev.startswith(ev_fqn + "."):
|
|
||||||
if ev not in ("event",) and "." in ev and not ev.startswith(ev_fqn):
|
|
||||||
issues.append(f"BUG: transitions[{i}].event {ev!r} not under {ev_fqn}")
|
|
||||||
for i, tr in enumerate(meta.get("triggers") or []):
|
|
||||||
for field in ("event", "sourceState"):
|
|
||||||
val = tr.get(field)
|
|
||||||
if not val or val in ("event", "payload", "customMessage", "eventProvider"):
|
|
||||||
continue
|
|
||||||
type_fqn = ev_fqn if field == "event" else st_fqn
|
|
||||||
if type_fqn and "." in val and not val.startswith("<") and not val.startswith(type_fqn + "."):
|
|
||||||
if field == "sourceState" and val not in graph and fn_form(val) not in {fn_form(x) for x in graph}:
|
|
||||||
issues.append(f"BUG: triggers[{i}].sourceState {val!r} not a machine state")
|
|
||||||
elif field == "event" and type_fqn and val.isupper() is False and "." not in val and not val.startswith("<"):
|
|
||||||
if val not in ("event", "payload", "customMessage", "eventProvider", "true", "false"):
|
|
||||||
issues.append(f"INFO: triggers[{i}].event dynamic {val!r}")
|
|
||||||
|
|
||||||
# matchedTransitions
|
|
||||||
for i, chain in enumerate(meta.get("callChains") or []):
|
|
||||||
for j, mt in enumerate(chain.get("matchedTransitions") or []):
|
|
||||||
me, ms, mtgt = mt.get("event"), mt.get("sourceState"), mt.get("targetState")
|
|
||||||
ok = False
|
|
||||||
for t in transitions:
|
|
||||||
if fn_form(eid(t.get("event"))) != fn_form(me):
|
|
||||||
continue
|
|
||||||
srcs = [sid(s) for s in t.get("sourceStates") or []]
|
|
||||||
tgts = [sid(s) for s in t.get("targetStates") or []]
|
|
||||||
if (not ms or any(fn_form(x) == fn_form(ms) for x in srcs)) and (
|
|
||||||
not mtgt or any(fn_form(x) == fn_form(mtgt) for x in tgts)
|
|
||||||
):
|
|
||||||
ok = True
|
|
||||||
break
|
|
||||||
if not ok:
|
|
||||||
issues.append(
|
|
||||||
f"BUG: callChains[{i}].matchedTransitions[{j}] ({ms!r},{me!r},{mtgt!r}) no transition"
|
|
||||||
)
|
|
||||||
|
|
||||||
# String machine: mixed NEW vs String.NEW
|
|
||||||
if is_string or st_fqn == "String":
|
|
||||||
all_ids = set(graph) | set(starts) | set(ends) | {sid(s) for s in states}
|
|
||||||
for x in list(all_ids):
|
|
||||||
if not x:
|
|
||||||
continue
|
|
||||||
bare = x.strip('"')
|
|
||||||
if bare and "." not in bare and f"String.{bare}" in all_ids:
|
|
||||||
issues.append(f"BUG: mixed bare {x!r} and String.{bare}")
|
|
||||||
|
|
||||||
# stateTypeFqn missing on enum machine
|
|
||||||
if is_pkg_enum and not st_fqn:
|
|
||||||
issues.append("BUG: enum machine missing stateTypeFqn")
|
|
||||||
|
|
||||||
return name, issues
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
total = 0
|
|
||||||
for path in sorted(GOLDEN.rglob("*.json")):
|
|
||||||
data = json.loads(path.read_text())
|
|
||||||
machine, issues = audit_file(path, data)
|
|
||||||
rel = path.relative_to(GOLDEN)
|
|
||||||
bugs = [i for i in issues if i.startswith("BUG")]
|
|
||||||
warns = [i for i in issues if i.startswith("WARN")]
|
|
||||||
noise = [i for i in issues if i.startswith("NOISE")]
|
|
||||||
info = [i for i in issues if i.startswith("INFO")]
|
|
||||||
if bugs or warns:
|
|
||||||
print(f"\n{'='*70}\n{rel}\n machine: {machine}")
|
|
||||||
for i in bugs + warns + noise + info:
|
|
||||||
print(f" [{i.split(':')[0]}] {i.split(': ', 1)[-1]}")
|
|
||||||
total += len(bugs) + len(warns)
|
|
||||||
print(f"\nTotal BUG+WARN: {total}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.validation.AnalysisCanonicalFormValidator;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Final enrichment step: fail fast when enum identifiers in the analysis model are not
|
|
||||||
* package-canonical. Disable with {@code -Danalyzer.canonical-form-validation.enabled=false}.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class AnalysisCanonicalFormEnricher implements AnalysisEnricher {
|
|
||||||
|
|
||||||
private final boolean enabled;
|
|
||||||
|
|
||||||
public AnalysisCanonicalFormEnricher() {
|
|
||||||
this.enabled = Boolean.parseBoolean(
|
|
||||||
System.getProperty("analyzer.canonical-form-validation.enabled", "true"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
|
||||||
if (!enabled || result == null || context == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.debug("Validating canonical enum identifiers for {}", result.getName());
|
|
||||||
AnalysisCanonicalFormValidator.enforce(result, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,15 +3,11 @@ package click.kamil.springstatemachineexporter.analysis.enricher;
|
|||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class CallChainEnricher implements AnalysisEnricher {
|
public class CallChainEnricher implements AnalysisEnricher {
|
||||||
@@ -19,28 +15,12 @@ public class CallChainEnricher implements AnalysisEnricher {
|
|||||||
@Override
|
@Override
|
||||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||||
log.info("Enriching {} with call chains", result.getName());
|
log.info("Enriching {} with call chains", result.getName());
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes =
|
|
||||||
StateMachineTypeResolver.resolveTypes(result.getName(), context);
|
|
||||||
|
|
||||||
List<TriggerPoint> canonicalTriggers = intelligence.findTriggerPoints().stream()
|
|
||||||
.map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
List<CallChain> chains = intelligence.findCallChains(
|
List<CallChain> chains = intelligence.findCallChains(
|
||||||
result.getMetadata().getEntryPoints(),
|
result.getMetadata().getEntryPoints(),
|
||||||
canonicalTriggers
|
result.getMetadata().getTriggers()
|
||||||
);
|
);
|
||||||
chains = chains.stream()
|
|
||||||
.map(chain -> chain.getTriggerPoint() == null
|
|
||||||
? chain
|
|
||||||
: chain.toBuilder()
|
|
||||||
.triggerPoint(MachineEnumCanonicalizer.canonicalizeTriggerPoint(
|
|
||||||
chain.getTriggerPoint(), machineTypes))
|
|
||||||
.build())
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
chains = MachineScopeFilter.filterCallChainsForMachine(chains, result.getName(), context);
|
|
||||||
|
|
||||||
result.addMetadata(CodebaseMetadata.builder()
|
result.addMetadata(CodebaseMetadata.builder()
|
||||||
.callChains(chains)
|
.callChains(chains)
|
||||||
.build());
|
.build());
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ public class EntryPointEnricher implements AnalysisEnricher {
|
|||||||
log.info("Enriching {} with entry points", result.getName());
|
log.info("Enriching {} with entry points", result.getName());
|
||||||
|
|
||||||
List<EntryPoint> entryPoints = intelligence.findEntryPoints();
|
List<EntryPoint> entryPoints = intelligence.findEntryPoints();
|
||||||
entryPoints = MachineScopeFilter.filterEntryPointsForMachine(entryPoints, result.getName(), context);
|
|
||||||
|
|
||||||
result.addMetadata(CodebaseMetadata.builder()
|
result.addMetadata(CodebaseMetadata.builder()
|
||||||
.entryPoints(entryPoints)
|
.entryPoints(entryPoints)
|
||||||
.build());
|
.build());
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters codebase-wide analysis artifacts to those relevant for a single state machine config.
|
|
||||||
*/
|
|
||||||
public final class MachineScopeFilter {
|
|
||||||
|
|
||||||
private static final BeanResolutionEngine ROUTING = new HeuristicBeanResolutionEngine();
|
|
||||||
|
|
||||||
private MachineScopeFilter() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<TriggerPoint> filterTriggersForMachine(
|
|
||||||
List<TriggerPoint> triggers, String machineName, CodebaseContext context) {
|
|
||||||
if (triggers == null || machineName == null) {
|
|
||||||
return triggers == null ? List.of() : triggers;
|
|
||||||
}
|
|
||||||
List<TriggerPoint> filtered = new ArrayList<>();
|
|
||||||
for (TriggerPoint trigger : triggers) {
|
|
||||||
if (isTriggerForMachine(trigger, machineName, context)) {
|
|
||||||
filtered.add(trigger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CallChain> filterCallChainsForMachine(
|
|
||||||
List<CallChain> chains, String machineName, CodebaseContext context) {
|
|
||||||
if (chains == null || machineName == null) {
|
|
||||||
return chains == null ? List.of() : chains;
|
|
||||||
}
|
|
||||||
List<CallChain> filtered = new ArrayList<>();
|
|
||||||
for (CallChain chain : chains) {
|
|
||||||
if (ROUTING.isRoutedToCorrectMachine(chain, machineName, context)) {
|
|
||||||
filtered.add(chain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<EntryPoint> filterEntryPointsForMachine(
|
|
||||||
List<EntryPoint> entryPoints, String machineName, CodebaseContext context) {
|
|
||||||
if (entryPoints == null || machineName == null) {
|
|
||||||
return entryPoints == null ? List.of() : entryPoints;
|
|
||||||
}
|
|
||||||
String machineDomain = extractMachineDomainKey(machineName);
|
|
||||||
List<EntryPoint> filtered = new ArrayList<>();
|
|
||||||
for (EntryPoint entryPoint : entryPoints) {
|
|
||||||
if (isEntryPointForMachine(entryPoint, machineDomain, machineName, context)) {
|
|
||||||
filtered.add(entryPoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isEntryPointForMachine(
|
|
||||||
EntryPoint entryPoint, String machineDomain, String machineName, CodebaseContext context) {
|
|
||||||
if (entryPoint == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String path = entryPoint.getMetadata() != null ? entryPoint.getMetadata().get("path") : null;
|
|
||||||
if (path != null) {
|
|
||||||
String pathDomain = inferDomainFromPath(path);
|
|
||||||
if (pathDomain != null && machineDomain != null && !domainsMatch(pathDomain, machineDomain)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (pathDomain == null && path.contains("{")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CallChain probe = CallChain.builder()
|
|
||||||
.entryPoint(entryPoint)
|
|
||||||
.methodChain(List.of(entryPoint.getClassName() + "." + entryPoint.getMethodName()))
|
|
||||||
.build();
|
|
||||||
return ROUTING.isRoutedToCorrectMachine(probe, machineName, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String inferDomainFromPath(String path) {
|
|
||||||
String lower = path.toLowerCase(Locale.ROOT);
|
|
||||||
if (lower.contains("/orders") || lower.contains("order.")) {
|
|
||||||
return "ORDER";
|
|
||||||
}
|
|
||||||
if (lower.contains("/documents") || lower.contains("document.")) {
|
|
||||||
return "DOCUMENT";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean domainsMatch(String pathDomain, String machineDomain) {
|
|
||||||
String normalizedPath = normalizeDomainKey(pathDomain);
|
|
||||||
String normalizedMachine = normalizeDomainKey(machineDomain);
|
|
||||||
return normalizedPath.equalsIgnoreCase(normalizedMachine)
|
|
||||||
|| normalizedMachine.startsWith(normalizedPath)
|
|
||||||
|| normalizedPath.startsWith(normalizedMachine);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String extractMachineDomainKey(String machineName) {
|
|
||||||
String simple = machineName.substring(machineName.lastIndexOf('.') + 1);
|
|
||||||
String upper = simple.toUpperCase(Locale.ROOT);
|
|
||||||
for (String marker : List.of("STATEMACHINECONFIGURATION", "STATEMACHINE", "CONFIGURATION", "CONFIG")) {
|
|
||||||
int idx = upper.indexOf(marker);
|
|
||||||
if (idx > 0) {
|
|
||||||
return normalizeDomainKey(upper.substring(0, idx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int stateIdx = upper.indexOf("STATE");
|
|
||||||
if (stateIdx > 0) {
|
|
||||||
return normalizeDomainKey(upper.substring(0, stateIdx));
|
|
||||||
}
|
|
||||||
return normalizeDomainKey(upper);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String normalizeDomainKey(String domain) {
|
|
||||||
if (domain == null || domain.isEmpty()) {
|
|
||||||
return domain;
|
|
||||||
}
|
|
||||||
String upper = domain.toUpperCase(Locale.ROOT);
|
|
||||||
if (upper.startsWith("STANDARD") && upper.length() > "STANDARD".length()) {
|
|
||||||
return upper.substring("STANDARD".length());
|
|
||||||
}
|
|
||||||
return upper;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isTriggerForMachine(TriggerPoint trigger, String machineName, CodebaseContext context) {
|
|
||||||
if (trigger == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
CallChain probe = CallChain.builder().triggerPoint(trigger).methodChain(List.of()).build();
|
|
||||||
return ROUTING.isRoutedToCorrectMachine(probe, machineName, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,6 @@ package click.kamil.springstatemachineexporter.analysis.enricher;
|
|||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.LifecycleTriggerMarkers;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||||
@@ -16,11 +14,10 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.matching.EventMatchingEngine;
|
import click.kamil.springstatemachineexporter.analysis.enricher.matching.EventMatchingEngine;
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.matching.StrictFqnMatchingEngine;
|
import click.kamil.springstatemachineexporter.analysis.enricher.matching.StrictFqnMatchingEngine;
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator;
|
|
||||||
|
|
||||||
public class TransitionLinkerEnricher implements AnalysisEnricher {
|
public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||||
|
|
||||||
@@ -43,11 +40,6 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (LifecycleTriggerMarkers.isLifecycle(tp.getEvent())) {
|
|
||||||
updatedChains.add(chain);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String triggerEvent = simplify(tp.getEvent());
|
String triggerEvent = simplify(tp.getEvent());
|
||||||
String triggerSource = tp.getSourceState() != null ? simplify(tp.getSourceState()) : null;
|
String triggerSource = tp.getSourceState() != null ? simplify(tp.getSourceState()) : null;
|
||||||
|
|
||||||
@@ -55,16 +47,17 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
|
|
||||||
for (Transition t : stateMachineTransitions) {
|
for (Transition t : stateMachineTransitions) {
|
||||||
if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp)) {
|
if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp)) {
|
||||||
String smEventForLink = canonicalEvent(t.getEvent());
|
String smEventRaw = t.getEvent().fullIdentifier() != null ? t.getEvent().fullIdentifier() : t.getEvent().rawName();
|
||||||
|
// Event matches
|
||||||
for (State smSourceState : t.getSourceStates()) {
|
for (State smSourceState : t.getSourceStates()) {
|
||||||
String smSourceForLink = canonicalState(smSourceState);
|
String smSourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName();
|
||||||
String smSource = simplify(smSourceForLink);
|
String smSource = simplify(smSourceRaw);
|
||||||
if (triggerSource == null || triggerSource.equals(smSource)) {
|
if (triggerSource == null || triggerSource.equals(smSource)) {
|
||||||
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
||||||
MatchedTransition mt = MatchedTransition.builder()
|
MatchedTransition mt = MatchedTransition.builder()
|
||||||
.sourceState(smSourceForLink)
|
.sourceState(smSourceRaw)
|
||||||
.targetState(smSourceForLink)
|
.targetState(smSourceRaw)
|
||||||
.event(smEventForLink)
|
.event(smEventRaw)
|
||||||
.build();
|
.build();
|
||||||
if (isRoutedToCorrectMachine(chain, result.getName(), context)
|
if (isRoutedToCorrectMachine(chain, result.getName(), context)
|
||||||
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
||||||
@@ -72,11 +65,11 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (State smTargetState : t.getTargetStates()) {
|
for (State smTargetState : t.getTargetStates()) {
|
||||||
String targetForLink = canonicalState(smTargetState);
|
String targetRaw = smTargetState.fullIdentifier() != null ? smTargetState.fullIdentifier() : smTargetState.rawName();
|
||||||
MatchedTransition mt = MatchedTransition.builder()
|
MatchedTransition mt = MatchedTransition.builder()
|
||||||
.sourceState(smSourceForLink)
|
.sourceState(smSourceRaw)
|
||||||
.targetState(targetForLink)
|
.targetState(targetRaw)
|
||||||
.event(smEventForLink)
|
.event(smEventRaw)
|
||||||
.build();
|
.build();
|
||||||
if (isRoutedToCorrectMachine(chain, result.getName(), context)
|
if (isRoutedToCorrectMachine(chain, result.getName(), context)
|
||||||
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
||||||
@@ -97,14 +90,10 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TriggerPoint> scopedTriggers = MachineScopeFilter.filterTriggersForMachine(
|
// Update the metadata with the new call chains
|
||||||
result.getMetadata().getTriggers(), result.getName(), context);
|
|
||||||
List<EntryPoint> scopedEntryPoints = MachineScopeFilter.filterEntryPointsForMachine(
|
|
||||||
result.getMetadata().getEntryPoints(), result.getName(), context);
|
|
||||||
|
|
||||||
CodebaseMetadata updatedMetadata = CodebaseMetadata.builder()
|
CodebaseMetadata updatedMetadata = CodebaseMetadata.builder()
|
||||||
.triggers(scopedTriggers)
|
.triggers(result.getMetadata().getTriggers())
|
||||||
.entryPoints(scopedEntryPoints)
|
.entryPoints(result.getMetadata().getEntryPoints())
|
||||||
.callChains(updatedChains)
|
.callChains(updatedChains)
|
||||||
.properties(result.getMetadata().getProperties())
|
.properties(result.getMetadata().getProperties())
|
||||||
.build();
|
.build();
|
||||||
@@ -118,30 +107,91 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isConstraintCompatible(String constraint, String machineName) {
|
private boolean isConstraintCompatible(String constraint, String machineName) {
|
||||||
if (constraint == null || machineName == null) {
|
if (constraint == null || machineName == null) return true;
|
||||||
return true;
|
|
||||||
|
String cleanMachine = machineName.substring(machineName.lastIndexOf('.') + 1).toUpperCase();
|
||||||
|
if (cleanMachine.endsWith("STATEMACHINECONFIGURATION")) {
|
||||||
|
cleanMachine = cleanMachine.substring(0, cleanMachine.length() - "STATEMACHINECONFIGURATION".length());
|
||||||
|
}
|
||||||
|
if (cleanMachine.endsWith("STATEMACHINE")) {
|
||||||
|
cleanMachine = cleanMachine.substring(0, cleanMachine.length() - "STATEMACHINE".length());
|
||||||
|
}
|
||||||
|
if (cleanMachine.endsWith("CONFIG")) {
|
||||||
|
cleanMachine = cleanMachine.substring(0, cleanMachine.length() - "CONFIG".length());
|
||||||
|
}
|
||||||
|
|
||||||
|
String expr = constraint;
|
||||||
|
java.util.regex.Pattern p = java.util.regex.Pattern.compile("[\"']([a-zA-Z0-9_-]+)[\"']");
|
||||||
|
java.util.regex.Matcher matcher = p.matcher(constraint);
|
||||||
|
java.util.Set<String> literals = new java.util.HashSet<>();
|
||||||
|
while (matcher.find()) {
|
||||||
|
literals.add(matcher.group(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String m : literals) {
|
||||||
|
boolean isCurrent = m.equalsIgnoreCase(cleanMachine);
|
||||||
|
String replacement = isCurrent ? "true" : "false";
|
||||||
|
|
||||||
|
String regex1 = "(?i)[\"']?" + m + "[\"']?\\s*\\.\\s*(equalsIgnoreCase|equals)\\s*\\([^)]*\\)";
|
||||||
|
String regex2 = "(?i)[a-zA-Z0-9._]+\\s*\\.\\s*(equalsIgnoreCase|equals)\\s*\\(\\s*[\"']?" + m + "[\"']?\\s*\\)";
|
||||||
|
String regex3 = "(?i)[a-zA-Z0-9._]+\\s*==\\s*[\"']?" + m + "[\"']?";
|
||||||
|
String regex4 = "(?i)[a-zA-Z0-9._]+\\s*!=\\s*[\"']?" + m + "[\"']?";
|
||||||
|
|
||||||
|
expr = expr.replaceAll(regex1, replacement);
|
||||||
|
expr = expr.replaceAll(regex2, replacement);
|
||||||
|
expr = expr.replaceAll(regex3, replacement);
|
||||||
|
expr = expr.replaceAll(regex4, isCurrent ? "false" : "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return BooleanEvaluator.evaluate(expr);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return !expr.contains("false");
|
||||||
}
|
}
|
||||||
return BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
|
|
||||||
constraint, extractMachineDomainKey(machineName));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String extractMachineDomainKey(String machineName) {
|
private static class BooleanEvaluator {
|
||||||
String simple = machineName.substring(machineName.lastIndexOf('.') + 1);
|
public static boolean evaluate(String expr) {
|
||||||
String upper = simple.toUpperCase();
|
expr = expr.replaceAll("\\s+", "");
|
||||||
|
return parseExpr(expr);
|
||||||
|
}
|
||||||
|
|
||||||
for (String marker : List.of("STATEMACHINECONFIGURATION", "STATEMACHINE", "CONFIGURATION", "CONFIG")) {
|
private static boolean parseExpr(String s) {
|
||||||
int idx = upper.indexOf(marker);
|
if (s.isEmpty()) return true;
|
||||||
if (idx > 0) {
|
|
||||||
return upper.substring(0, idx);
|
int parenDepth = 0;
|
||||||
|
for (int i = s.length() - 1; i >= 0; i--) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
if (c == ')') parenDepth++;
|
||||||
|
else if (c == '(') parenDepth--;
|
||||||
|
else if (parenDepth == 0 && i > 0 && s.charAt(i) == '|' && s.charAt(i - 1) == '|') {
|
||||||
|
return parseExpr(s.substring(0, i - 1)) || parseExpr(s.substring(i + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parenDepth = 0;
|
||||||
|
for (int i = s.length() - 1; i >= 0; i--) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
if (c == ')') parenDepth++;
|
||||||
|
else if (c == '(') parenDepth--;
|
||||||
|
else if (parenDepth == 0 && i > 0 && s.charAt(i) == '&' && s.charAt(i - 1) == '&') {
|
||||||
|
return parseExpr(s.substring(0, i - 1)) && parseExpr(s.substring(i + 1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
int stateIdx = upper.indexOf("STATE");
|
if (s.startsWith("!")) {
|
||||||
if (stateIdx > 0) {
|
return !parseExpr(s.substring(1));
|
||||||
return upper.substring(0, stateIdx);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return upper;
|
if (s.startsWith("(") && s.endsWith(")")) {
|
||||||
|
return parseExpr(s.substring(1, s.length() - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.equalsIgnoreCase("true")) return true;
|
||||||
|
if (s.equalsIgnoreCase("false")) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final java.util.Map<String, String> simplifyCache = new java.util.HashMap<>();
|
private final java.util.Map<String, String> simplifyCache = new java.util.HashMap<>();
|
||||||
@@ -165,20 +215,6 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
return simplified;
|
return simplified;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String canonicalState(State state) {
|
|
||||||
if (state == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return state.fullIdentifier() != null ? state.fullIdentifier() : state.rawName();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String canonicalEvent(click.kamil.springstatemachineexporter.model.Event event) {
|
|
||||||
if (event == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return event.fullIdentifier() != null ? event.fullIdentifier() : event.rawName();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isGuardedContains(String smEvent, String triggerEvent) {
|
private boolean isGuardedContains(String smEvent, String triggerEvent) {
|
||||||
if (smEvent == null || triggerEvent == null) return false;
|
if (smEvent == null || triggerEvent == null) return false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Canonicalizes trigger-side enum identifiers ({@code event}, {@code polymorphicEvents},
|
|
||||||
* {@code sourceState}) using the owning machine config's {@code <State, Event>} types.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class TriggerCanonicalizationEnricher implements AnalysisEnricher {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
|
||||||
if (result.getMetadata() == null || context == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes =
|
|
||||||
StateMachineTypeResolver.resolveTypes(result.getName(), context);
|
|
||||||
|
|
||||||
List<TriggerPoint> triggers = result.getMetadata().getTriggers();
|
|
||||||
List<TriggerPoint> canonicalTriggers = triggers == null ? null : triggers.stream()
|
|
||||||
.map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
List<CallChain> callChains = result.getMetadata().getCallChains();
|
|
||||||
List<CallChain> canonicalChains = callChains == null ? null : callChains.stream()
|
|
||||||
.map(chain -> {
|
|
||||||
if (chain.getTriggerPoint() == null) {
|
|
||||||
return chain;
|
|
||||||
}
|
|
||||||
return chain.toBuilder()
|
|
||||||
.triggerPoint(MachineEnumCanonicalizer.canonicalizeTriggerPoint(
|
|
||||||
chain.getTriggerPoint(), machineTypes))
|
|
||||||
.build();
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (canonicalTriggers == null && canonicalChains == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("Canonicalized triggers for {}", result.getName());
|
|
||||||
|
|
||||||
result.setMetadata(CodebaseMetadata.builder()
|
|
||||||
.triggers(canonicalTriggers != null ? canonicalTriggers : triggers)
|
|
||||||
.entryPoints(result.getMetadata().getEntryPoints())
|
|
||||||
.callChains(canonicalChains != null ? canonicalChains : callChains)
|
|
||||||
.properties(result.getMetadata().getProperties())
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,11 @@ package click.kamil.springstatemachineexporter.analysis.enricher;
|
|||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class TriggerEnricher implements AnalysisEnricher {
|
public class TriggerEnricher implements AnalysisEnricher {
|
||||||
@@ -17,15 +14,11 @@ public class TriggerEnricher implements AnalysisEnricher {
|
|||||||
@Override
|
@Override
|
||||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||||
log.info("Enriching {} with triggers", result.getName());
|
log.info("Enriching {} with triggers", result.getName());
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes =
|
List<TriggerPoint> triggers = intelligence.findTriggerPoints();
|
||||||
StateMachineTypeResolver.resolveTypes(result.getName(), context);
|
|
||||||
|
// Initially, we just add all triggers found in the codebase to the metadata.
|
||||||
List<TriggerPoint> triggers = intelligence.findTriggerPoints().stream()
|
// In later steps of Phase 2, we will filter them by State Machine ID.
|
||||||
.map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
triggers = MachineScopeFilter.filterTriggersForMachine(triggers, result.getName(), context);
|
|
||||||
|
|
||||||
result.addMetadata(click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
|
result.addMetadata(click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
|
||||||
.triggers(triggers)
|
.triggers(triggers)
|
||||||
.build());
|
.build());
|
||||||
|
|||||||
@@ -12,51 +12,56 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (click.kamil.springstatemachineexporter.analysis.model.LifecycleTriggerMarkers
|
if (triggerPoint.isExternal()) {
|
||||||
.isLifecycle(triggerPoint.getEvent())) {
|
return true;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String smEventRaw = stateMachineEvent.fullIdentifier() != null ? stateMachineEvent.fullIdentifier() : stateMachineEvent.rawName();
|
String smEventRaw = stateMachineEvent.fullIdentifier() != null ? stateMachineEvent.fullIdentifier() : stateMachineEvent.rawName();
|
||||||
String rawTriggerEvent = triggerPoint.getEvent();
|
String rawTriggerEvent = triggerPoint.getEvent();
|
||||||
|
|
||||||
if (triggerPoint.isExternal()) {
|
|
||||||
List<String> polyEvents = triggerPoint.getPolymorphicEvents() != null
|
|
||||||
? triggerPoint.getPolymorphicEvents()
|
|
||||||
: java.util.Collections.emptyList();
|
|
||||||
if (polyEvents.isEmpty() && isDynamicVariable(rawTriggerEvent)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rawTriggerEvent != null && rawTriggerEvent.startsWith("<SYMBOLIC: ") && rawTriggerEvent.endsWith(".*>")) {
|
if (rawTriggerEvent != null && rawTriggerEvent.startsWith("<SYMBOLIC: ") && rawTriggerEvent.endsWith(".*>")) {
|
||||||
|
String targetType = rawTriggerEvent.substring(11, rawTriggerEvent.length() - 3);
|
||||||
|
if (smEventRaw.startsWith(targetType + ".")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (smEventRaw.contains(".")) {
|
||||||
|
String smType = smEventRaw.substring(0, smEventRaw.lastIndexOf('.'));
|
||||||
|
if (smType.endsWith("." + targetType) || smType.equals(targetType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> polyEvents = triggerPoint.getPolymorphicEvents() != null ? triggerPoint.getPolymorphicEvents() : java.util.Collections.emptyList();
|
List<String> polyEvents = triggerPoint.getPolymorphicEvents() != null ? triggerPoint.getPolymorphicEvents() : java.util.Collections.emptyList();
|
||||||
|
|
||||||
for (String pe : polyEvents) {
|
for (String pe : polyEvents) {
|
||||||
if (pe.startsWith("<SYMBOLIC: ") && pe.endsWith(".*>")) {
|
if (matchEventNames(pe, smEventRaw, triggerPoint.getEventTypeFqn())) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String eventTypeFqn = triggerPoint.getEventTypeFqn();
|
|
||||||
if (pe.contains(".") && isStringTypeOrPrimitive(eventTypeFqn)) {
|
|
||||||
eventTypeFqn = null;
|
|
||||||
}
|
|
||||||
if (matchEventNames(pe, smEventRaw, eventTypeFqn)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!pe.contains(".") && smEventRaw.contains(".")) {
|
if (pe.startsWith("<SYMBOLIC: ") && pe.endsWith(".*>")) {
|
||||||
String smConst = constantName(smEventRaw);
|
String targetType = pe.substring(11, pe.length() - 3);
|
||||||
if (pe.equals(smConst) && triggerPoint.getEventTypeFqn() != null
|
if (smEventRaw.startsWith(targetType + ".")) {
|
||||||
&& !isStringTypeOrPrimitive(triggerPoint.getEventTypeFqn())) {
|
return true;
|
||||||
String smEnumType = smEventRaw.substring(0, smEventRaw.lastIndexOf('.'));
|
}
|
||||||
return enumTypesMatch(triggerPoint.getEventTypeFqn(), smEnumType);
|
if (smEventRaw.contains(".")) {
|
||||||
|
String smType = smEventRaw.substring(0, smEventRaw.lastIndexOf('.'));
|
||||||
|
if (smType.endsWith("." + targetType) || smType.equals(targetType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rawTriggerEvent != null && rawTriggerEvent.contains(".valueOf(")) {
|
if (rawTriggerEvent != null && rawTriggerEvent.contains(".valueOf(")) {
|
||||||
|
String enumClass = rawTriggerEvent.substring(0, rawTriggerEvent.indexOf(".valueOf("));
|
||||||
|
if (enumClass.contains(" ")) {
|
||||||
|
enumClass = enumClass.substring(enumClass.lastIndexOf(" ") + 1);
|
||||||
|
}
|
||||||
|
String smClass = smEventRaw.contains(".") ? smEventRaw.substring(0, smEventRaw.lastIndexOf('.')) : smEventRaw;
|
||||||
|
if (enumClass.equals(smClass) || enumClass.endsWith("." + smClass) || smClass.endsWith("." + enumClass)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,13 +85,13 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine {
|
|||||||
if (triggerEvent == null || smEvent == null) return false;
|
if (triggerEvent == null || smEvent == null) return false;
|
||||||
if (triggerEvent.equals(smEvent)) return true;
|
if (triggerEvent.equals(smEvent)) return true;
|
||||||
|
|
||||||
String tConst = constantName(triggerEvent);
|
String tConst = triggerEvent.contains(".") ? triggerEvent.substring(triggerEvent.lastIndexOf('.') + 1) : triggerEvent;
|
||||||
String smConst = constantName(smEvent);
|
String smConst = smEvent.contains(".") ? smEvent.substring(smEvent.lastIndexOf('.') + 1) : smEvent;
|
||||||
|
|
||||||
if (!tConst.equals(smConst)) return false;
|
if (!tConst.equals(smConst)) return false;
|
||||||
|
|
||||||
// Prevent matching string/primitive triggers to enum transitions
|
// Prevent matching string/primitive triggers to enum transitions
|
||||||
if (isStringTypeOrPrimitive(eventTypeFqn) && smEvent.contains(".") && !triggerEvent.contains(".")) {
|
if (isStringTypeOrPrimitive(eventTypeFqn) && smEvent.contains(".")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,46 +100,20 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (smEvent.contains(".")) {
|
if (eventTypeFqn != null && eventTypeFqn.contains(".") && smEvent.contains(".")) {
|
||||||
String smEnumType = smEvent.substring(0, smEvent.lastIndexOf('.'));
|
String fullTriggerFqn = eventTypeFqn + "." + tConst;
|
||||||
|
return fullTriggerFqn.equals(smEvent);
|
||||||
if (eventTypeFqn != null) {
|
|
||||||
return enumTypesMatch(eventTypeFqn, smEnumType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (triggerEvent.contains(".")) {
|
|
||||||
String triggerEnumType = triggerEvent.substring(0, triggerEvent.lastIndexOf('.'));
|
|
||||||
return enumTypesMatch(triggerEnumType, smEnumType);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bare constant name (e.g. after Enum.name()) with no type context must not match every enum
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Both sides are unqualified identifiers (string/primitive events)
|
if (triggerEvent.contains(".") && smEvent.contains(".")) {
|
||||||
return !triggerEvent.contains(".");
|
String tClass = triggerEvent.substring(0, triggerEvent.lastIndexOf('.'));
|
||||||
}
|
String smClass = smEvent.substring(0, smEvent.lastIndexOf('.'));
|
||||||
|
String tClassSimple = tClass.contains(".") ? tClass.substring(tClass.lastIndexOf('.') + 1) : tClass;
|
||||||
|
String smClassSimple = smClass.contains(".") ? smClass.substring(smClass.lastIndexOf('.') + 1) : smClass;
|
||||||
|
return tClassSimple.equals(smClassSimple);
|
||||||
|
}
|
||||||
|
|
||||||
private String constantName(String event) {
|
return true;
|
||||||
return event.contains(".") ? event.substring(event.lastIndexOf('.') + 1) : event;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean enumTypesMatch(String type1, String type2) {
|
|
||||||
if (type1 == null || type2 == null) return false;
|
|
||||||
if (type1.equals(type2)) return true;
|
|
||||||
if (type1.endsWith("." + type2) || type2.endsWith("." + type1)) return true;
|
|
||||||
|
|
||||||
String simple1 = simpleName(type1);
|
|
||||||
String simple2 = simpleName(type2);
|
|
||||||
if (!simple1.equals(simple2)) return false;
|
|
||||||
|
|
||||||
// Same simple name only matches when at least one side is package-less (import-style reference).
|
|
||||||
// Prevents com.foo.OrderEvents from matching com.bar.OrderEvents.
|
|
||||||
return !type1.contains(".") || !type2.contains(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
private String simpleName(String name) {
|
|
||||||
return name.contains(".") ? name.substring(name.lastIndexOf('.') + 1) : name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isStringTypeOrPrimitive(String typeFqn) {
|
private boolean isStringTypeOrPrimitive(String typeFqn) {
|
||||||
@@ -155,13 +134,9 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (eventStr.contains(".")) {
|
if (eventStr.contains(".")) {
|
||||||
String constantPart = eventStr.substring(eventStr.lastIndexOf('.') + 1);
|
|
||||||
if (!constantPart.isEmpty() && Character.isUpperCase(constantPart.charAt(0))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String firstPart = eventStr.substring(0, eventStr.indexOf('.'));
|
String firstPart = eventStr.substring(0, eventStr.indexOf('.'));
|
||||||
if (!firstPart.isEmpty() && Character.isLowerCase(firstPart.charAt(0))) {
|
if (!firstPart.isEmpty() && Character.isLowerCase(firstPart.charAt(0))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -17,18 +15,17 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
|||||||
String triggerEventFqn = chain.getTriggerPoint().getEventTypeFqn();
|
String triggerEventFqn = chain.getTriggerPoint().getEventTypeFqn();
|
||||||
String triggerStateFqn = chain.getTriggerPoint().getStateTypeFqn();
|
String triggerStateFqn = chain.getTriggerPoint().getStateTypeFqn();
|
||||||
if (triggerEventFqn != null || triggerStateFqn != null) {
|
if (triggerEventFqn != null || triggerStateFqn != null) {
|
||||||
String[] machineTypes = StateMachineTypeResolver.resolve(currentMachineName, context);
|
String[] machineTypes = getStateMachineTypeArguments(currentMachineName, context);
|
||||||
String machineStateFqn = machineTypes[0];
|
String machineStateFqn = machineTypes[0];
|
||||||
String machineEventFqn = machineTypes[1];
|
String machineEventFqn = machineTypes[1];
|
||||||
|
|
||||||
if (triggerEventFqn != null && machineEventFqn != null) {
|
if (triggerEventFqn != null && machineEventFqn != null) {
|
||||||
if (eraseGenerics(triggerEventFqn).equals(eraseGenerics(machineEventFqn))) {
|
if (eraseGenerics(triggerEventFqn).equals(eraseGenerics(machineEventFqn))) {
|
||||||
return true;
|
return true; // Match!
|
||||||
}
|
}
|
||||||
if (isTypeMismatched(triggerEventFqn, machineEventFqn)) {
|
if (isTypeMismatched(triggerEventFqn, machineEventFqn)) {
|
||||||
return false;
|
return false; // Mismatch!
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
if (triggerStateFqn != null && machineStateFqn != null) {
|
if (triggerStateFqn != null && machineStateFqn != null) {
|
||||||
if (eraseGenerics(triggerStateFqn).equals(eraseGenerics(machineStateFqn))) {
|
if (eraseGenerics(triggerStateFqn).equals(eraseGenerics(machineStateFqn))) {
|
||||||
@@ -113,51 +110,15 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return context == null || isSingleStateMachine(context);
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
private int countStateMachines(CodebaseContext context) {
|
|
||||||
if (context == null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
Set<String> machines = new HashSet<>();
|
|
||||||
for (TypeDeclaration td : context.findEntryPointClasses(
|
|
||||||
java.util.List.of("EnableStateMachineFactory", "EnableStateMachine"))) {
|
|
||||||
String fqn = context.getFqn(td);
|
|
||||||
if (fqn != null) {
|
|
||||||
machines.add(fqn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (TypeDeclaration td : context.getTypeDeclarations()) {
|
|
||||||
if (td == null || td.isInterface()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (context.extendsStateMachineConfigurerAdapter(td)) {
|
|
||||||
String fqn = context.getFqn(td);
|
|
||||||
if (fqn != null) {
|
|
||||||
machines.add(fqn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!machines.isEmpty()) {
|
|
||||||
return machines.size();
|
|
||||||
}
|
|
||||||
return context.findBeanMethodsReturning(
|
|
||||||
java.util.Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory")).size();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isDomainMatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) {
|
private boolean isDomainMatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) {
|
||||||
if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) {
|
if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false;
|
||||||
return false;
|
|
||||||
}
|
if (smPackage.startsWith(chainPackage) || chainPackage.startsWith(smPackage)) {
|
||||||
|
|
||||||
if (smPackage.equals(chainPackage)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chainPackage.startsWith(smPackage + ".") && !smPackage.startsWith(chainPackage + ".")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find deepest common package root
|
// Find deepest common package root
|
||||||
String[] p1 = smPackage.split("\\.");
|
String[] p1 = smPackage.split("\\.");
|
||||||
@@ -300,6 +261,59 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String[] getStateMachineTypeArguments(String machineName, CodebaseContext context) {
|
||||||
|
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration(machineName);
|
||||||
|
if (td != null) {
|
||||||
|
org.eclipse.jdt.core.dom.Type superclass = td.getSuperclassType();
|
||||||
|
Set<String> visited = new HashSet<>();
|
||||||
|
visited.add(machineName);
|
||||||
|
return findTypeArgumentsRecursively(superclass, context, (org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot(), visited);
|
||||||
|
}
|
||||||
|
return new String[]{null, null};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] findTypeArgumentsRecursively(org.eclipse.jdt.core.dom.Type type, CodebaseContext context, org.eclipse.jdt.core.dom.CompilationUnit cu, Set<String> visited) {
|
||||||
|
if (type == null) return new String[]{null, null};
|
||||||
|
|
||||||
|
if (type instanceof org.eclipse.jdt.core.dom.ParameterizedType pt) {
|
||||||
|
String rawTypeName = pt.getType().toString();
|
||||||
|
if (rawTypeName.equals("StateMachineConfigurerAdapter") || rawTypeName.equals("EnumStateMachineConfigurerAdapter") ||
|
||||||
|
rawTypeName.endsWith(".StateMachineConfigurerAdapter") || rawTypeName.endsWith(".EnumStateMachineConfigurerAdapter")) {
|
||||||
|
java.util.List<?> typeArgs = pt.typeArguments();
|
||||||
|
if (typeArgs.size() >= 2) {
|
||||||
|
String stateType = resolveTypeToFqn((org.eclipse.jdt.core.dom.Type) typeArgs.get(0), cu, context);
|
||||||
|
String eventType = resolveTypeToFqn((org.eclipse.jdt.core.dom.Type) typeArgs.get(1), cu, context);
|
||||||
|
return new String[]{stateType, eventType};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse into the raw type declaration's superclass
|
||||||
|
org.eclipse.jdt.core.dom.AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(rawTypeName, cu);
|
||||||
|
if (td != null) {
|
||||||
|
String fqn = context.getFqn(td);
|
||||||
|
if (visited.add(fqn)) {
|
||||||
|
org.eclipse.jdt.core.dom.Type superclass = (td instanceof org.eclipse.jdt.core.dom.TypeDeclaration) ? ((org.eclipse.jdt.core.dom.TypeDeclaration) td).getSuperclassType() : null;
|
||||||
|
String[] res = findTypeArgumentsRecursively(superclass, context, (org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot(), visited);
|
||||||
|
if (res[0] != null || res[1] != null) return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// It's a simple type (e.g. MyBaseConfig)
|
||||||
|
String simpleName = type.toString();
|
||||||
|
org.eclipse.jdt.core.dom.AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
||||||
|
if (td != null) {
|
||||||
|
String fqn = context.getFqn(td);
|
||||||
|
if (visited.add(fqn)) {
|
||||||
|
org.eclipse.jdt.core.dom.Type superclass = (td instanceof org.eclipse.jdt.core.dom.TypeDeclaration) ? ((org.eclipse.jdt.core.dom.TypeDeclaration) td).getSuperclassType() : null;
|
||||||
|
String[] res = findTypeArgumentsRecursively(superclass, context, (org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot(), visited);
|
||||||
|
if (res[0] != null || res[1] != null) return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String[]{null, null};
|
||||||
|
}
|
||||||
|
|
||||||
private String eraseGenerics(String type) {
|
private String eraseGenerics(String type) {
|
||||||
if (type == null) return null;
|
if (type == null) return null;
|
||||||
int idx = type.indexOf('<');
|
int idx = type.indexOf('<');
|
||||||
@@ -309,6 +323,43 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
|||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolveTypeToFqn(org.eclipse.jdt.core.dom.Type type, org.eclipse.jdt.core.dom.CompilationUnit cu, CodebaseContext context) {
|
||||||
|
if (type == null) return null;
|
||||||
|
String simpleName;
|
||||||
|
if (type.isSimpleType()) {
|
||||||
|
simpleName = ((org.eclipse.jdt.core.dom.SimpleType) type).getName().getFullyQualifiedName();
|
||||||
|
} else if (type.isParameterizedType()) {
|
||||||
|
return resolveTypeToFqn(((org.eclipse.jdt.core.dom.ParameterizedType) type).getType(), cu, context);
|
||||||
|
} else {
|
||||||
|
simpleName = type.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (simpleName.contains("<")) {
|
||||||
|
simpleName = simpleName.substring(0, simpleName.indexOf('<'));
|
||||||
|
}
|
||||||
|
|
||||||
|
org.eclipse.jdt.core.dom.AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
||||||
|
if (td != null) {
|
||||||
|
return context.getFqn(td);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Object impObj : cu.imports()) {
|
||||||
|
org.eclipse.jdt.core.dom.ImportDeclaration imp = (org.eclipse.jdt.core.dom.ImportDeclaration) impObj;
|
||||||
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
|
if (impName.endsWith("." + simpleName)) {
|
||||||
|
return impName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
||||||
|
if (!packageName.isEmpty()) {
|
||||||
|
org.eclipse.jdt.core.dom.AbstractTypeDeclaration localTd = context.getAbstractTypeDeclaration(packageName + "." + simpleName);
|
||||||
|
if (localTd != null) return context.getFqn(localTd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return simpleName;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isTypeMismatched(String type1, String type2) {
|
private boolean isTypeMismatched(String type1, String type2) {
|
||||||
if (type1 == null || type2 == null) return false;
|
if (type1 == null || type2 == null) return false;
|
||||||
type1 = eraseGenerics(type1);
|
type1 = eraseGenerics(type1);
|
||||||
@@ -320,6 +371,10 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isSingleStateMachine(CodebaseContext context) {
|
private boolean isSingleStateMachine(CodebaseContext context) {
|
||||||
return countStateMachines(context) <= 1;
|
if (context == null) return false;
|
||||||
|
int count = 0;
|
||||||
|
count += context.findEntryPointClasses(java.util.List.of("EnableStateMachineFactory", "EnableStateMachine")).size();
|
||||||
|
count += context.findBeanMethodsReturning(java.util.Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory")).size();
|
||||||
|
return count <= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
|
||||||
import org.eclipse.jdt.core.dom.FieldDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.BiConsumer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves constant values for fields referenced by indexed trivial accessors.
|
|
||||||
*/
|
|
||||||
public final class AccessorFieldResolver {
|
|
||||||
|
|
||||||
private AccessorFieldResolver() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<String> resolveFieldConstants(
|
|
||||||
AccessorSummary accessor,
|
|
||||||
CodebaseContext context,
|
|
||||||
ConstructorAnalyzer constructorAnalyzer,
|
|
||||||
CompilationUnit contextCu,
|
|
||||||
Set<String> visited,
|
|
||||||
BiConsumer<org.eclipse.jdt.core.dom.Expression, List<String>> constantExtractor) {
|
|
||||||
if (accessor == null || !accessor.isGetter() || context == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
if (accessor.kind() == click.kamil.springstatemachineexporter.analysis.index.AccessorKind.CONSTANT_GETTER
|
|
||||||
|| accessor.fieldName() == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
TypeDeclaration typeDeclaration = contextCu != null
|
|
||||||
? context.getTypeDeclaration(accessor.declaringFqn(), contextCu)
|
|
||||||
: null;
|
|
||||||
if (typeDeclaration == null) {
|
|
||||||
typeDeclaration = context.getTypeDeclaration(accessor.declaringFqn());
|
|
||||||
}
|
|
||||||
if (typeDeclaration == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> constants = new ArrayList<>();
|
|
||||||
if (constructorAnalyzer != null) {
|
|
||||||
constants.addAll(constructorAnalyzer.traceFieldInConstructors(
|
|
||||||
typeDeclaration, accessor.fieldName(), context, visited));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (constants.isEmpty()) {
|
|
||||||
collectFieldInitializerConstants(
|
|
||||||
typeDeclaration, accessor.fieldName(), constantExtractor, constants);
|
|
||||||
}
|
|
||||||
return constants;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void collectFieldInitializerConstants(
|
|
||||||
TypeDeclaration typeDeclaration,
|
|
||||||
String fieldName,
|
|
||||||
BiConsumer<org.eclipse.jdt.core.dom.Expression, List<String>> constantExtractor,
|
|
||||||
List<String> constants) {
|
|
||||||
if (constantExtractor == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (FieldDeclaration fieldDeclaration : typeDeclaration.getFields()) {
|
|
||||||
for (Object fragmentObj : fieldDeclaration.fragments()) {
|
|
||||||
if (fragmentObj instanceof VariableDeclarationFragment fragment
|
|
||||||
&& fragment.getName().getIdentifier().equals(fieldName)
|
|
||||||
&& fragment.getInitializer() != null) {
|
|
||||||
constantExtractor.accept(fragment.getInitializer(), constants);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class AccessorIndex {
|
|
||||||
|
|
||||||
private static final AccessorIndex EMPTY = new AccessorIndex(Map.of(), 0, 0, 0);
|
|
||||||
|
|
||||||
private final Map<String, Map<String, AccessorSummary>> byOwnerAndMethod;
|
|
||||||
private final int typeCount;
|
|
||||||
private final int trivialCount;
|
|
||||||
private final int blockedCount;
|
|
||||||
|
|
||||||
public AccessorIndex(
|
|
||||||
Map<String, Map<String, AccessorSummary>> byOwnerAndMethod,
|
|
||||||
int typeCount,
|
|
||||||
int trivialCount,
|
|
||||||
int blockedCount) {
|
|
||||||
this.byOwnerAndMethod = Collections.unmodifiableMap(deepCopy(byOwnerAndMethod));
|
|
||||||
this.typeCount = typeCount;
|
|
||||||
this.trivialCount = trivialCount;
|
|
||||||
this.blockedCount = blockedCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static AccessorIndex empty() {
|
|
||||||
return EMPTY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<AccessorSummary> lookup(String ownerFqn, String methodName) {
|
|
||||||
if (ownerFqn == null || methodName == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Map<String, AccessorSummary> methods = byOwnerAndMethod.get(ownerFqn);
|
|
||||||
if (methods == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return Optional.ofNullable(methods.get(methodName));
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<AccessorSummary> accessorsForType(String ownerFqn) {
|
|
||||||
Map<String, AccessorSummary> methods = byOwnerAndMethod.get(ownerFqn);
|
|
||||||
if (methods == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
return List.copyOf(methods.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, Map<String, AccessorSummary>> asMap() {
|
|
||||||
return byOwnerAndMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int typeCount() {
|
|
||||||
return typeCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int trivialCount() {
|
|
||||||
return trivialCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int blockedCount() {
|
|
||||||
return blockedCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, Map<String, AccessorSummary>> deepCopy(Map<String, Map<String, AccessorSummary>> source) {
|
|
||||||
Map<String, Map<String, AccessorSummary>> copy = new HashMap<>();
|
|
||||||
for (Map.Entry<String, Map<String, AccessorSummary>> entry : source.entrySet()) {
|
|
||||||
copy.put(entry.getKey(), Map.copyOf(entry.getValue()));
|
|
||||||
}
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.eclipse.jdt.core.dom.*;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class AccessorIndexBuilder {
|
|
||||||
|
|
||||||
private final TrivialAccessorDetector detector = new TrivialAccessorDetector();
|
|
||||||
private CodebaseContext context;
|
|
||||||
|
|
||||||
public AccessorIndex build(CodebaseContext context) {
|
|
||||||
this.context = context;
|
|
||||||
Map<String, DirectTypeAccessors> direct = new HashMap<>();
|
|
||||||
|
|
||||||
for (CompilationUnit cu : context.getCompilationUnits()) {
|
|
||||||
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
|
||||||
for (Object typeObj : cu.types()) {
|
|
||||||
if (typeObj instanceof TypeDeclaration td) {
|
|
||||||
indexTypeDeclaration(td, direct);
|
|
||||||
} else if (typeObj instanceof RecordDeclaration rd) {
|
|
||||||
indexRecord(rd, packageName, direct);
|
|
||||||
} else if (typeObj instanceof EnumDeclaration ed) {
|
|
||||||
indexEnum(ed, packageName, direct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int blocked = direct.values().stream().mapToInt(state -> state.blockedMethods.size()).sum();
|
|
||||||
|
|
||||||
Map<String, Map<String, AccessorSummary>> effective = new HashMap<>();
|
|
||||||
int trivial = 0;
|
|
||||||
for (String ownerFqn : direct.keySet()) {
|
|
||||||
Map<String, AccessorSummary> merged = mergeEffective(ownerFqn, direct, effective, new HashSet<>());
|
|
||||||
if (!merged.isEmpty()) {
|
|
||||||
effective.put(ownerFqn, merged);
|
|
||||||
trivial += merged.size();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
"Built accessor index: {} trivial accessors across {} types ({} blocked overrides)",
|
|
||||||
trivial,
|
|
||||||
effective.size(),
|
|
||||||
blocked);
|
|
||||||
|
|
||||||
this.context = null;
|
|
||||||
return new AccessorIndex(effective, effective.size(), trivial, blocked);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void indexTypeDeclaration(TypeDeclaration td, Map<String, DirectTypeAccessors> direct) {
|
|
||||||
String ownerFqn = context.getFqn(td);
|
|
||||||
DirectTypeAccessors state = direct.computeIfAbsent(ownerFqn, k -> new DirectTypeAccessors());
|
|
||||||
for (MethodDeclaration md : td.getMethods()) {
|
|
||||||
classifyMethod(ownerFqn, md, fieldName -> findFieldDeclaringFqn(td, fieldName), state);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (TypeDeclaration nested : td.getTypes()) {
|
|
||||||
indexTypeDeclaration(nested, direct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void indexEnum(EnumDeclaration ed, String parentFqn, Map<String, DirectTypeAccessors> direct) {
|
|
||||||
String ownerFqn = parentFqn.isEmpty() ? ed.getName().getIdentifier() : parentFqn + "." + ed.getName().getIdentifier();
|
|
||||||
DirectTypeAccessors state = direct.computeIfAbsent(ownerFqn, k -> new DirectTypeAccessors());
|
|
||||||
|
|
||||||
for (Object decl : ed.bodyDeclarations()) {
|
|
||||||
if (decl instanceof MethodDeclaration md) {
|
|
||||||
classifyMethod(ownerFqn, md, fieldName -> findEnumFieldDeclaringFqn(ed, ownerFqn, fieldName), state);
|
|
||||||
} else if (decl instanceof TypeDeclaration nestedTd) {
|
|
||||||
indexTypeDeclaration(nestedTd, direct);
|
|
||||||
} else if (decl instanceof RecordDeclaration nestedRd) {
|
|
||||||
indexRecord(nestedRd, ownerFqn, direct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void indexRecord(RecordDeclaration rd, String parentFqn, Map<String, DirectTypeAccessors> direct) {
|
|
||||||
String ownerFqn = parentFqn.isEmpty() ? rd.getName().getIdentifier() : parentFqn + "." + rd.getName().getIdentifier();
|
|
||||||
DirectTypeAccessors state = direct.computeIfAbsent(ownerFqn, k -> new DirectTypeAccessors());
|
|
||||||
|
|
||||||
for (Object compObj : rd.recordComponents()) {
|
|
||||||
String componentName = recordComponentName(compObj);
|
|
||||||
if (componentName == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
detector.detectRecordComponent(ownerFqn, componentName).ifPresent(summary ->
|
|
||||||
state.trivial.put(summary.methodName(), summary));
|
|
||||||
}
|
|
||||||
|
|
||||||
TrivialAccessorDetector.FieldLookup recordFieldLookup =
|
|
||||||
fieldName -> findRecordFieldDeclaringFqn(rd, ownerFqn, fieldName);
|
|
||||||
|
|
||||||
for (Object declObj : rd.bodyDeclarations()) {
|
|
||||||
if (declObj instanceof MethodDeclaration md) {
|
|
||||||
String methodName = md.getName().getIdentifier();
|
|
||||||
AccessorSummary existing = state.trivial.get(methodName);
|
|
||||||
if (existing != null && existing.kind() == AccessorKind.RECORD_COMPONENT) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
classifyMethod(ownerFqn, md, recordFieldLookup, state);
|
|
||||||
} else if (declObj instanceof TypeDeclaration nestedTd) {
|
|
||||||
indexTypeDeclaration(nestedTd, direct);
|
|
||||||
} else if (declObj instanceof RecordDeclaration nestedRd) {
|
|
||||||
indexRecord(nestedRd, ownerFqn, direct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void classifyMethod(
|
|
||||||
String ownerFqn,
|
|
||||||
MethodDeclaration md,
|
|
||||||
TrivialAccessorDetector.FieldLookup fieldLookup,
|
|
||||||
DirectTypeAccessors state) {
|
|
||||||
if (md.getBody() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String methodName = md.getName().getIdentifier();
|
|
||||||
Optional<AccessorSummary> summary = detector.detect(ownerFqn, md, fieldLookup);
|
|
||||||
applyClassification(state, methodName, summary);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyClassification(DirectTypeAccessors state, String methodName, Optional<AccessorSummary> summary) {
|
|
||||||
if (summary.isPresent()) {
|
|
||||||
state.trivial.put(methodName, summary.get());
|
|
||||||
state.blockedMethods.remove(methodName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (looksLikeAccessorMethod(methodName)) {
|
|
||||||
state.blockedMethods.add(methodName);
|
|
||||||
state.trivial.remove(methodName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean looksLikeAccessorMethod(String methodName) {
|
|
||||||
return methodName.startsWith("get")
|
|
||||||
|| methodName.startsWith("set")
|
|
||||||
|| methodName.startsWith("is");
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, AccessorSummary> mergeEffective(
|
|
||||||
String ownerFqn,
|
|
||||||
Map<String, DirectTypeAccessors> direct,
|
|
||||||
Map<String, Map<String, AccessorSummary>> cache,
|
|
||||||
Set<String> visiting) {
|
|
||||||
ownerFqn = resolveTypeFqn(ownerFqn);
|
|
||||||
if (ownerFqn == null) {
|
|
||||||
return Map.of();
|
|
||||||
}
|
|
||||||
if (cache.containsKey(ownerFqn)) {
|
|
||||||
return cache.get(ownerFqn);
|
|
||||||
}
|
|
||||||
if (!visiting.add(ownerFqn)) {
|
|
||||||
return Map.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, AccessorSummary> merged = new HashMap<>();
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(ownerFqn);
|
|
||||||
if (td != null) {
|
|
||||||
String superFqn = resolveTypeFqn(context.getSuperclassFqn(td));
|
|
||||||
if (superFqn != null && !"java.lang.Object".equals(superFqn)) {
|
|
||||||
for (Map.Entry<String, AccessorSummary> inherited : mergeEffective(superFqn, direct, cache, visiting).entrySet()) {
|
|
||||||
merged.put(inherited.getKey(), rebindOwner(inherited.getValue(), ownerFqn));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DirectTypeAccessors own = direct.get(ownerFqn);
|
|
||||||
if (own != null) {
|
|
||||||
own.blockedMethods.forEach(merged::remove);
|
|
||||||
merged.putAll(own.trivial);
|
|
||||||
}
|
|
||||||
|
|
||||||
visiting.remove(ownerFqn);
|
|
||||||
cache.put(ownerFqn, Map.copyOf(merged));
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private AccessorSummary rebindOwner(AccessorSummary summary, String ownerFqn) {
|
|
||||||
if (summary.ownerFqn().equals(ownerFqn)) {
|
|
||||||
return summary;
|
|
||||||
}
|
|
||||||
return new AccessorSummary(
|
|
||||||
ownerFqn,
|
|
||||||
summary.methodName(),
|
|
||||||
summary.kind(),
|
|
||||||
summary.fieldName(),
|
|
||||||
summary.paramName(),
|
|
||||||
summary.declaringFqn(),
|
|
||||||
summary.returnsThis()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveTypeFqn(String typeName) {
|
|
||||||
if (typeName == null || typeName.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (typeName.contains(".")) {
|
|
||||||
return typeName;
|
|
||||||
}
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(typeName);
|
|
||||||
return td != null ? context.getFqn(td) : typeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class DirectTypeAccessors {
|
|
||||||
private final Map<String, AccessorSummary> trivial = new HashMap<>();
|
|
||||||
private final Set<String> blockedMethods = new HashSet<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<String> findFieldDeclaringFqn(TypeDeclaration td, String fieldName) {
|
|
||||||
return findFieldDeclaringFqn(td, fieldName, new HashSet<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<String> findFieldDeclaringFqn(TypeDeclaration td, String fieldName, Set<String> visited) {
|
|
||||||
if (td == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
String ownerFqn = context.getFqn(td);
|
|
||||||
if (!visited.add(ownerFqn)) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (FieldDeclaration fd : td.getFields()) {
|
|
||||||
for (Object fragObj : fd.fragments()) {
|
|
||||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
|
||||||
if (frag.getName().getIdentifier().equals(fieldName)) {
|
|
||||||
return Optional.of(ownerFqn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String superFqn = context.getSuperclassFqn(td);
|
|
||||||
if (superFqn != null && !"java.lang.Object".equals(superFqn)) {
|
|
||||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
|
||||||
if (superTd != null) {
|
|
||||||
return findFieldDeclaringFqn(superTd, fieldName, visited);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<String> findEnumFieldDeclaringFqn(EnumDeclaration ed, String ownerFqn, String fieldName) {
|
|
||||||
for (Object decl : ed.bodyDeclarations()) {
|
|
||||||
if (decl instanceof FieldDeclaration fd) {
|
|
||||||
for (Object fragObj : fd.fragments()) {
|
|
||||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
|
||||||
if (frag.getName().getIdentifier().equals(fieldName)) {
|
|
||||||
return Optional.of(ownerFqn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<String> findRecordFieldDeclaringFqn(RecordDeclaration rd, String ownerFqn, String fieldName) {
|
|
||||||
for (Object compObj : rd.recordComponents()) {
|
|
||||||
String componentName = recordComponentName(compObj);
|
|
||||||
if (fieldName.equals(componentName)) {
|
|
||||||
return Optional.of(ownerFqn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String recordComponentName(Object compObj) {
|
|
||||||
if (compObj instanceof SingleVariableDeclaration svd) {
|
|
||||||
return svd.getName().getIdentifier();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
java.lang.reflect.Method getNameMethod = compObj.getClass().getMethod("getName");
|
|
||||||
Object nameObj = getNameMethod.invoke(compObj);
|
|
||||||
if (nameObj instanceof SimpleName sn) {
|
|
||||||
return sn.getIdentifier();
|
|
||||||
}
|
|
||||||
} catch (ReflectiveOperationException ignored) {
|
|
||||||
// fall through
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
|
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
|
||||||
import org.eclipse.jdt.core.dom.IMethodBinding;
|
|
||||||
import org.eclipse.jdt.core.dom.ITypeBinding;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public final class AccessorInlining {
|
|
||||||
|
|
||||||
private AccessorInlining() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Optional<AccessorSummary> lookupAccessor(
|
|
||||||
CodebaseContext context,
|
|
||||||
MethodInvocation mi,
|
|
||||||
List<Expression> receiverDefinitions) {
|
|
||||||
if (context == null || mi == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
String methodName = mi.getName().getIdentifier();
|
|
||||||
if (isLikelyNonBeanGet(methodName, mi)) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (receiverDefinitions != null) {
|
|
||||||
for (Expression receiverDef : receiverDefinitions) {
|
|
||||||
if (receiverDef instanceof ClassInstanceCreation cic) {
|
|
||||||
Optional<AccessorSummary> concrete = lookupOnType(context, resolveTypeFqn(cic), methodName);
|
|
||||||
if (concrete.isPresent()) {
|
|
||||||
return concrete;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IMethodBinding methodBinding = mi.resolveMethodBinding();
|
|
||||||
if (methodBinding != null && methodBinding.getDeclaringClass() != null) {
|
|
||||||
ITypeBinding declaringClass = methodBinding.getDeclaringClass().getErasure();
|
|
||||||
Optional<AccessorSummary> declared = lookupOnType(context, declaringClass.getQualifiedName(), methodName);
|
|
||||||
if (declared.isPresent()) {
|
|
||||||
return declared;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Optional<AccessorSummary> lookupAccessor(CodebaseContext context, MethodInvocation mi) {
|
|
||||||
return lookupAccessor(context, mi, List.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Optional<AccessorSummary> lookupOnType(CodebaseContext context, String ownerFqn, String methodName) {
|
|
||||||
if (ownerFqn == null || ownerFqn.isBlank() || ownerFqn.startsWith("java.")) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return context.getAccessorIndex().lookup(ownerFqn, methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String resolveTypeFqn(ClassInstanceCreation cic) {
|
|
||||||
if (cic == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (cic.resolveConstructorBinding() != null && cic.resolveConstructorBinding().getDeclaringClass() != null) {
|
|
||||||
return cic.resolveConstructorBinding().getDeclaringClass().getErasure().getQualifiedName();
|
|
||||||
}
|
|
||||||
if (cic.resolveTypeBinding() != null) {
|
|
||||||
return cic.resolveTypeBinding().getErasure().getQualifiedName();
|
|
||||||
}
|
|
||||||
return AstUtils.extractSimpleTypeName(cic.getType());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isLikelyNonBeanGet(String methodName, MethodInvocation mi) {
|
|
||||||
if (!"get".equals(methodName) && !"getOrDefault".equals(methodName)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (mi.arguments().size() != 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
IMethodBinding methodBinding = mi.resolveMethodBinding();
|
|
||||||
if (methodBinding == null || methodBinding.getDeclaringClass() == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String ownerFqn = methodBinding.getDeclaringClass().getErasure().getQualifiedName();
|
|
||||||
return ownerFqn != null && ownerFqn.startsWith("java.util");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
public enum AccessorKind {
|
|
||||||
GETTER,
|
|
||||||
BOOLEAN_GETTER,
|
|
||||||
CONSTANT_GETTER,
|
|
||||||
RECORD_COMPONENT,
|
|
||||||
SETTER,
|
|
||||||
FLUENT_SETTER
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
public record AccessorSummary(
|
|
||||||
String ownerFqn,
|
|
||||||
String methodName,
|
|
||||||
AccessorKind kind,
|
|
||||||
String fieldName,
|
|
||||||
String paramName,
|
|
||||||
String declaringFqn,
|
|
||||||
boolean returnsThis
|
|
||||||
) {
|
|
||||||
public boolean isGetter() {
|
|
||||||
return kind == AccessorKind.GETTER
|
|
||||||
|| kind == AccessorKind.BOOLEAN_GETTER
|
|
||||||
|| kind == AccessorKind.CONSTANT_GETTER
|
|
||||||
|| kind == AccessorKind.RECORD_COMPONENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSetter() {
|
|
||||||
return kind == AccessorKind.SETTER || kind == AccessorKind.FLUENT_SETTER;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import org.eclipse.jdt.core.dom.*;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detects trivial JavaBean-style getters and setters whose bodies only read or write a single field.
|
|
||||||
*/
|
|
||||||
public class TrivialAccessorDetector {
|
|
||||||
|
|
||||||
@FunctionalInterface
|
|
||||||
public interface FieldLookup extends Function<String, Optional<String>> {
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<AccessorSummary> detect(String ownerFqn, MethodDeclaration method, FieldLookup fieldLookup) {
|
|
||||||
if (ownerFqn == null || method == null || method.getBody() == null || fieldLookup == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
String methodName = method.getName().getIdentifier();
|
|
||||||
if (method.isConstructor()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<String> getterField = propertyNameForGetter(methodName, method);
|
|
||||||
if (getterField.isPresent()) {
|
|
||||||
Optional<AccessorSummary> fieldGetter = detectGetter(ownerFqn, method, methodName, getterField.get(), fieldLookup);
|
|
||||||
if (fieldGetter.isPresent()) {
|
|
||||||
return fieldGetter;
|
|
||||||
}
|
|
||||||
return detectConstantGetter(ownerFqn, method, methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<String> setterField = propertyNameForSetter(methodName);
|
|
||||||
if (setterField.isPresent() && !method.parameters().isEmpty()) {
|
|
||||||
return detectSetter(ownerFqn, method, methodName, setterField.get(), fieldLookup);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<AccessorSummary> detectRecordComponent(String ownerFqn, String componentName) {
|
|
||||||
if (ownerFqn == null || componentName == null || componentName.isEmpty()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return Optional.of(new AccessorSummary(
|
|
||||||
ownerFqn,
|
|
||||||
componentName,
|
|
||||||
AccessorKind.RECORD_COMPONENT,
|
|
||||||
componentName,
|
|
||||||
null,
|
|
||||||
ownerFqn,
|
|
||||||
false
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<AccessorSummary> detectConstantGetter(
|
|
||||||
String ownerFqn,
|
|
||||||
MethodDeclaration method,
|
|
||||||
String methodName) {
|
|
||||||
List<?> statements = method.getBody().statements();
|
|
||||||
if (statements.size() != 1 || !(statements.get(0) instanceof ReturnStatement rs)) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Expression expr = rs.getExpression();
|
|
||||||
if (expr == null || !isConstantReturnExpression(expr)) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return Optional.of(new AccessorSummary(
|
|
||||||
ownerFqn,
|
|
||||||
methodName,
|
|
||||||
AccessorKind.CONSTANT_GETTER,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
ownerFqn,
|
|
||||||
false
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isConstantReturnExpression(Expression expr) {
|
|
||||||
Expression current = unwrap(expr);
|
|
||||||
if (current instanceof StringLiteral
|
|
||||||
|| current instanceof NumberLiteral
|
|
||||||
|| current instanceof BooleanLiteral
|
|
||||||
|| current instanceof CharacterLiteral) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (current instanceof QualifiedName) {
|
|
||||||
return !containsMethodCall(current);
|
|
||||||
}
|
|
||||||
if (current instanceof SimpleName) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (current instanceof FieldAccess fa) {
|
|
||||||
Expression receiver = fa.getExpression();
|
|
||||||
return (receiver instanceof SimpleName || receiver instanceof QualifiedName)
|
|
||||||
&& !containsMethodCall(current);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<AccessorSummary> detectGetter(
|
|
||||||
String ownerFqn,
|
|
||||||
MethodDeclaration method,
|
|
||||||
String methodName,
|
|
||||||
String fieldName,
|
|
||||||
FieldLookup fieldLookup) {
|
|
||||||
if (!isTrivialGetterBody(method.getBody(), fieldName)) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Optional<String> declaringFqn = fieldLookup.apply(fieldName);
|
|
||||||
if (declaringFqn.isEmpty()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
AccessorKind kind = methodName.startsWith("is") ? AccessorKind.BOOLEAN_GETTER : AccessorKind.GETTER;
|
|
||||||
return Optional.of(new AccessorSummary(
|
|
||||||
ownerFqn,
|
|
||||||
methodName,
|
|
||||||
kind,
|
|
||||||
fieldName,
|
|
||||||
null,
|
|
||||||
declaringFqn.get(),
|
|
||||||
false
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<AccessorSummary> detectSetter(
|
|
||||||
String ownerFqn,
|
|
||||||
MethodDeclaration method,
|
|
||||||
String methodName,
|
|
||||||
String fieldName,
|
|
||||||
FieldLookup fieldLookup) {
|
|
||||||
SingleVariableDeclaration param = (SingleVariableDeclaration) method.parameters().get(0);
|
|
||||||
String paramName = param.getName().getIdentifier();
|
|
||||||
|
|
||||||
boolean fluent = isTrivialFluentSetterBody(method.getBody(), fieldName, paramName);
|
|
||||||
boolean voidSetter = isVoidReturn(method) && isTrivialVoidSetterBody(method.getBody(), fieldName, paramName);
|
|
||||||
if (!fluent && !voidSetter) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<String> declaringFqn = fieldLookup.apply(fieldName);
|
|
||||||
if (declaringFqn.isEmpty()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
AccessorKind kind = fluent ? AccessorKind.FLUENT_SETTER : AccessorKind.SETTER;
|
|
||||||
return Optional.of(new AccessorSummary(
|
|
||||||
ownerFqn,
|
|
||||||
methodName,
|
|
||||||
kind,
|
|
||||||
fieldName,
|
|
||||||
paramName,
|
|
||||||
declaringFqn.get(),
|
|
||||||
fluent
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<String> propertyNameForGetter(String methodName, MethodDeclaration method) {
|
|
||||||
if (methodName.startsWith("get") && methodName.length() > 3) {
|
|
||||||
return Optional.of(decapitalize(methodName.substring(3)));
|
|
||||||
}
|
|
||||||
if (methodName.startsWith("is") && methodName.length() > 2 && isBooleanReturn(method)) {
|
|
||||||
return Optional.of(decapitalize(methodName.substring(2)));
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<String> propertyNameForSetter(String methodName) {
|
|
||||||
if (methodName.startsWith("set") && methodName.length() > 3) {
|
|
||||||
return Optional.of(decapitalize(methodName.substring(3)));
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isBooleanReturn(MethodDeclaration method) {
|
|
||||||
return "boolean".equals(method.getReturnType2().toString())
|
|
||||||
|| "Boolean".equals(method.getReturnType2().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isVoidReturn(MethodDeclaration method) {
|
|
||||||
return method.getReturnType2() instanceof PrimitiveType pt && pt.getPrimitiveTypeCode() == PrimitiveType.VOID;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String decapitalize(String name) {
|
|
||||||
if (name == null || name.isEmpty()) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
if (name.length() > 1 && Character.isUpperCase(name.charAt(1))) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
return Character.toLowerCase(name.charAt(0)) + name.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTrivialGetterBody(Block body, String fieldName) {
|
|
||||||
List<?> statements = body.statements();
|
|
||||||
if (statements.size() != 1 || !(statements.get(0) instanceof ReturnStatement rs)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Expression expr = rs.getExpression();
|
|
||||||
if (expr == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return isDirectFieldRead(unwrap(expr), fieldName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTrivialVoidSetterBody(Block body, String fieldName, String paramName) {
|
|
||||||
List<?> statements = body.statements();
|
|
||||||
if (statements.size() != 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return isAssignmentStatement(statements.get(0), fieldName, paramName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTrivialFluentSetterBody(Block body, String fieldName, String paramName) {
|
|
||||||
List<?> statements = body.statements();
|
|
||||||
if (statements.size() != 2) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!isAssignmentStatement(statements.get(0), fieldName, paramName)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!(statements.get(1) instanceof ReturnStatement rs)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Expression ret = unwrap(rs.getExpression());
|
|
||||||
return ret instanceof ThisExpression;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isAssignmentStatement(Object statement, String fieldName, String paramName) {
|
|
||||||
if (!(statement instanceof ExpressionStatement es)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!(es.getExpression() instanceof Assignment assignment)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (assignment.getOperator() != Assignment.Operator.ASSIGN) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!isDirectFieldWrite(assignment.getLeftHandSide(), fieldName)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Expression rhs = unwrap(assignment.getRightHandSide());
|
|
||||||
return rhs instanceof SimpleName sn && sn.getIdentifier().equals(paramName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDirectFieldRead(Expression expr, String fieldName) {
|
|
||||||
if (containsMethodCall(expr)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (expr instanceof SimpleName sn) {
|
|
||||||
return sn.getIdentifier().equals(fieldName);
|
|
||||||
}
|
|
||||||
if (expr instanceof FieldAccess fa) {
|
|
||||||
Expression receiver = fa.getExpression();
|
|
||||||
if (receiver == null || receiver instanceof ThisExpression) {
|
|
||||||
return fa.getName().getIdentifier().equals(fieldName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDirectFieldWrite(Expression expr, String fieldName) {
|
|
||||||
if (expr instanceof SimpleName sn) {
|
|
||||||
return sn.getIdentifier().equals(fieldName);
|
|
||||||
}
|
|
||||||
if (expr instanceof FieldAccess fa) {
|
|
||||||
Expression receiver = fa.getExpression();
|
|
||||||
if (receiver == null || receiver instanceof ThisExpression) {
|
|
||||||
return fa.getName().getIdentifier().equals(fieldName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean containsMethodCall(Expression expr) {
|
|
||||||
if (expr == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
final boolean[] found = {false};
|
|
||||||
expr.accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(MethodInvocation node) {
|
|
||||||
found[0] = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return found[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private Expression unwrap(Expression expr) {
|
|
||||||
Expression current = expr;
|
|
||||||
while (current instanceof ParenthesizedExpression pe) {
|
|
||||||
current = pe.getExpression();
|
|
||||||
}
|
|
||||||
while (current instanceof CastExpression ce) {
|
|
||||||
current = ce.getExpression();
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,10 +3,6 @@ package click.kamil.springstatemachineexporter.analysis.model;
|
|||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
|
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -35,11 +31,6 @@ public class AnalysisResult {
|
|||||||
@Builder.Default
|
@Builder.Default
|
||||||
private List<BusinessFlow> flows = java.util.Collections.emptyList();
|
private List<BusinessFlow> flows = java.util.Collections.emptyList();
|
||||||
|
|
||||||
/** Resolved {@code StateMachineConfigurerAdapter<S,E>} state type; persisted for JSON round-trip. */
|
|
||||||
private String stateTypeFqn;
|
|
||||||
/** Resolved {@code StateMachineConfigurerAdapter<S,E>} event type; persisted for JSON round-trip. */
|
|
||||||
private String eventTypeFqn;
|
|
||||||
|
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private CodebaseMetadata metadata = CodebaseMetadata.empty();
|
private CodebaseMetadata metadata = CodebaseMetadata.empty();
|
||||||
|
|
||||||
@@ -61,9 +52,7 @@ public class AnalysisResult {
|
|||||||
// 2. Resolve states (State record is immutable, so recreate)
|
// 2. Resolve states (State record is immutable, so recreate)
|
||||||
if (states != null) {
|
if (states != null) {
|
||||||
this.states = states.stream()
|
this.states = states.stream()
|
||||||
.map(s -> click.kamil.springstatemachineexporter.model.State.of(
|
.map(s -> click.kamil.springstatemachineexporter.model.State.of(s.rawName(), resolver.resolveValue(s.fullIdentifier(), properties)))
|
||||||
resolver.resolveValue(s.rawName(), properties),
|
|
||||||
resolver.resolveValue(s.fullIdentifier(), properties)))
|
|
||||||
.collect(java.util.stream.Collectors.toSet());
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,18 +72,15 @@ public class AnalysisResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Resolve metadata (triggers, entry points, call chains)
|
// 4. Resolve metadata (triggers, entry points)
|
||||||
if (metadata != null) {
|
if (metadata != null) {
|
||||||
List<TriggerPoint> resolvedTriggers = metadata.getTriggers() == null ? null
|
if (metadata.getTriggers() != null) {
|
||||||
: metadata.getTriggers().stream()
|
for (var trigger : metadata.getTriggers()) {
|
||||||
.map(trigger -> resolveTrigger(trigger, resolver, properties))
|
if (trigger.getEvent() != null) {
|
||||||
.collect(java.util.stream.Collectors.toList());
|
trigger.setEvent(resolver.resolveValue(trigger.getEvent(), properties));
|
||||||
|
}
|
||||||
List<CallChain> resolvedCallChains = metadata.getCallChains() == null ? null
|
}
|
||||||
: metadata.getCallChains().stream()
|
}
|
||||||
.map(chain -> resolveCallChain(chain, resolver, properties))
|
|
||||||
.collect(java.util.stream.Collectors.toList());
|
|
||||||
|
|
||||||
if (metadata.getEntryPoints() != null) {
|
if (metadata.getEntryPoints() != null) {
|
||||||
for (var ep : metadata.getEntryPoints()) {
|
for (var ep : metadata.getEntryPoints()) {
|
||||||
if (ep.getName() != null) {
|
if (ep.getName() != null) {
|
||||||
@@ -109,54 +95,9 @@ public class AnalysisResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.metadata = CodebaseMetadata.builder()
|
|
||||||
.triggers(resolvedTriggers != null ? resolvedTriggers : metadata.getTriggers())
|
|
||||||
.entryPoints(metadata.getEntryPoints())
|
|
||||||
.callChains(resolvedCallChains != null ? resolvedCallChains : metadata.getCallChains())
|
|
||||||
.properties(metadata.getProperties())
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TriggerPoint resolveTrigger(
|
|
||||||
TriggerPoint trigger,
|
|
||||||
click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver resolver,
|
|
||||||
Map<String, String> properties) {
|
|
||||||
List<String> polymorphicEvents = trigger.getPolymorphicEvents() == null ? null
|
|
||||||
: trigger.getPolymorphicEvents().stream()
|
|
||||||
.map(value -> resolver.resolveValue(value, properties))
|
|
||||||
.collect(java.util.stream.Collectors.toList());
|
|
||||||
return trigger.toBuilder()
|
|
||||||
.event(trigger.getEvent() != null ? resolver.resolveValue(trigger.getEvent(), properties) : null)
|
|
||||||
.sourceState(trigger.getSourceState() != null
|
|
||||||
? resolver.resolveValue(trigger.getSourceState(), properties)
|
|
||||||
: null)
|
|
||||||
.polymorphicEvents(polymorphicEvents)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CallChain resolveCallChain(
|
|
||||||
CallChain chain,
|
|
||||||
click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver resolver,
|
|
||||||
Map<String, String> properties) {
|
|
||||||
TriggerPoint trigger = chain.getTriggerPoint() == null
|
|
||||||
? null
|
|
||||||
: resolveTrigger(chain.getTriggerPoint(), resolver, properties);
|
|
||||||
List<MatchedTransition> matchedTransitions = chain.getMatchedTransitions() == null ? null
|
|
||||||
: chain.getMatchedTransitions().stream()
|
|
||||||
.map(matched -> MatchedTransition.builder()
|
|
||||||
.event(resolver.resolveValue(matched.getEvent(), properties))
|
|
||||||
.sourceState(resolver.resolveValue(matched.getSourceState(), properties))
|
|
||||||
.targetState(resolver.resolveValue(matched.getTargetState(), properties))
|
|
||||||
.build())
|
|
||||||
.collect(java.util.stream.Collectors.toList());
|
|
||||||
return chain.toBuilder()
|
|
||||||
.triggerPoint(trigger)
|
|
||||||
.matchedTransitions(matchedTransitions)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addMetadata(CodebaseMetadata newMetadata) {
|
public void addMetadata(CodebaseMetadata newMetadata) {
|
||||||
if (newMetadata == null) return;
|
if (newMetadata == null) return;
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synthetic trigger markers for persistence/lifecycle operations that do not fire
|
|
||||||
* {@code sendEvent} transitions (e.g. {@code StateMachinePersister.restore(...)}).
|
|
||||||
*/
|
|
||||||
public final class LifecycleTriggerMarkers {
|
|
||||||
|
|
||||||
public static final String PREFIX = "[LIFECYCLE:";
|
|
||||||
|
|
||||||
private LifecycleTriggerMarkers() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isLifecycle(String event) {
|
|
||||||
return event != null && event.startsWith(PREFIX) && event.endsWith("]");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String lifecycleType(String event) {
|
|
||||||
if (!isLifecycle(event)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return event.substring(PREFIX.length(), event.length() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String displayLabel(String event) {
|
|
||||||
String type = lifecycleType(event);
|
|
||||||
if (type == null) {
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
return switch (type.toUpperCase()) {
|
|
||||||
case "RESTORE" -> "Restore persisted state";
|
|
||||||
case "RESET" -> "Reset state machine";
|
|
||||||
default -> "Lifecycle: " + type.toLowerCase();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.model;
|
package click.kamil.springstatemachineexporter.analysis.model;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -50,14 +49,14 @@ public class TriggerPoint {
|
|||||||
this.sourceFile = sourceFile;
|
this.sourceFile = sourceFile;
|
||||||
this.sourceModule = sourceModule;
|
this.sourceModule = sourceModule;
|
||||||
this.stateMachineId = stateMachineId;
|
this.stateMachineId = stateMachineId;
|
||||||
this.sourceState = MachineEnumCanonicalizer.canonicalizeLabel(sourceState, stateTypeFqn);
|
this.sourceState = sourceState;
|
||||||
this.lineNumber = lineNumber;
|
this.lineNumber = lineNumber;
|
||||||
this.stateTypeFqn = stateTypeFqn;
|
this.stateTypeFqn = stateTypeFqn;
|
||||||
this.eventTypeFqn = eventTypeFqn;
|
this.eventTypeFqn = eventTypeFqn;
|
||||||
this.event = MachineEnumCanonicalizer.qualifyEventIdentifier(event, eventTypeFqn);
|
this.event = qualifyEvent(event, eventTypeFqn);
|
||||||
if (polymorphicEvents != null) {
|
if (polymorphicEvents != null) {
|
||||||
this.polymorphicEvents = polymorphicEvents.stream()
|
this.polymorphicEvents = polymorphicEvents.stream()
|
||||||
.map(pe -> MachineEnumCanonicalizer.qualifyEventIdentifier(pe, eventTypeFqn))
|
.map(pe -> qualifyEvent(pe, eventTypeFqn))
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
} else {
|
} else {
|
||||||
this.polymorphicEvents = null;
|
this.polymorphicEvents = null;
|
||||||
@@ -66,4 +65,66 @@ public class TriggerPoint {
|
|||||||
this.constraint = constraint;
|
this.constraint = constraint;
|
||||||
this.ambiguous = ambiguous;
|
this.ambiguous = ambiguous;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String qualifyEvent(String e, String eventTypeFqn) {
|
||||||
|
if (e == null || eventTypeFqn == null || e.isEmpty()) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
if (e.startsWith("<SYMBOLIC: ")) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
if (isWildcardVariable(e)) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
if (Character.isLowerCase(e.charAt(0))) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
if (!isEnumConstantCandidate(e)) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
String simpleEventType = eventTypeFqn.contains(".") ? eventTypeFqn.substring(eventTypeFqn.lastIndexOf('.') + 1) : eventTypeFqn;
|
||||||
|
|
||||||
|
if (e.contains(".")) {
|
||||||
|
String qualifier = e.substring(0, e.lastIndexOf('.'));
|
||||||
|
String lastSegment = e.substring(e.lastIndexOf('.') + 1);
|
||||||
|
String simpleQualifier = qualifier.contains(".") ? qualifier.substring(qualifier.lastIndexOf('.') + 1) : qualifier;
|
||||||
|
|
||||||
|
if (simpleQualifier.equals(simpleEventType)) {
|
||||||
|
return simpleEventType + "." + lastSegment;
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
if (eventTypeFqn.startsWith("java.lang.") || eventTypeFqn.equals("String") || eventTypeFqn.equals("int") || eventTypeFqn.equals("long") || eventTypeFqn.equals("char")) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
return simpleEventType + "." + e;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isEnumConstantCandidate(String eventStr) {
|
||||||
|
if (eventStr == null) return false;
|
||||||
|
if (eventStr.startsWith("<SYMBOLIC: ")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (eventStr.contains(" ") || eventStr.contains("(") || eventStr.contains(")") || eventStr.contains("?")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isWildcardVariable(String eventStr) {
|
||||||
|
if (eventStr == null) return false;
|
||||||
|
if (eventStr.equals("event") || eventStr.equals("e") ||
|
||||||
|
eventStr.equals("msg") || eventStr.equals("message") ||
|
||||||
|
eventStr.equals("payload")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (eventStr.contains(".valueOf(") || eventStr.contains("valueOf (")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (eventStr.contains(" ? ") && eventStr.contains(" : ")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,433 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorKind;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorFieldResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
|
||||||
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
|
|
||||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.BiConsumer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unified accessor resolution pipeline: index lookup first, then field/CIC/method-body fallbacks.
|
|
||||||
*/
|
|
||||||
public final class AccessorResolver {
|
|
||||||
|
|
||||||
public record GetterChainStep(String typeFqn, ClassInstanceCreation cic) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public record ReceiverContext(
|
|
||||||
TypeDeclaration ownerType,
|
|
||||||
ClassInstanceCreation cic,
|
|
||||||
CompilationUnit contextCu,
|
|
||||||
boolean anonymousGetter,
|
|
||||||
Map<String, String> fieldValues) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private final CodebaseContext context;
|
|
||||||
private final ConstantExtractor constantExtractor;
|
|
||||||
private final ConstructorAnalyzer constructorAnalyzer;
|
|
||||||
private final ConstantResolver constantResolver;
|
|
||||||
private final FieldInitializerFinder fieldInitializerFinder;
|
|
||||||
private final Map<String, List<String>> sessionCache = new HashMap<>();
|
|
||||||
|
|
||||||
public AccessorResolver(
|
|
||||||
CodebaseContext context,
|
|
||||||
ConstantExtractor constantExtractor,
|
|
||||||
ConstructorAnalyzer constructorAnalyzer,
|
|
||||||
ConstantResolver constantResolver) {
|
|
||||||
this.context = context;
|
|
||||||
this.constantExtractor = constantExtractor;
|
|
||||||
this.constructorAnalyzer = constructorAnalyzer;
|
|
||||||
this.constantResolver = constantResolver;
|
|
||||||
this.fieldInitializerFinder = new FieldInitializerFinder(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<AccessorSummary> lookupIndexedGetter(String className, String methodName) {
|
|
||||||
if (className == null || className.isBlank()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Optional<AccessorSummary> direct = context.getAccessorIndex().lookup(className, methodName);
|
|
||||||
if (direct.isPresent()) {
|
|
||||||
return direct;
|
|
||||||
}
|
|
||||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(className);
|
|
||||||
if (typeDeclaration != null) {
|
|
||||||
direct = context.getAccessorIndex().lookup(context.getFqn(typeDeclaration), methodName);
|
|
||||||
if (direct.isPresent()) {
|
|
||||||
return direct;
|
|
||||||
}
|
|
||||||
if (!typeDeclaration.isInterface() && !org.eclipse.jdt.core.dom.Modifier.isAbstract(typeDeclaration.getModifiers())) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
List<String> implementations = context.getImplementations(className);
|
|
||||||
if (implementations == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
for (String implementation : implementations) {
|
|
||||||
Optional<AccessorSummary> implAccessor = context.getAccessorIndex().lookup(implementation, methodName);
|
|
||||||
if (implAccessor.isPresent()) {
|
|
||||||
return implAccessor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> resolve(String ownerFqn, String methodName, ReceiverContext receiverContext, ResolutionBudget budget) {
|
|
||||||
return resolve(ownerFqn, methodName, receiverContext, budget, new HashSet<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> resolve(
|
|
||||||
String ownerFqn,
|
|
||||||
String methodName,
|
|
||||||
ReceiverContext receiverContext,
|
|
||||||
ResolutionBudget budget,
|
|
||||||
Set<String> visited) {
|
|
||||||
if (ownerFqn == null || methodName == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
if (budget == null) {
|
|
||||||
budget = ResolutionBudget.defaults();
|
|
||||||
}
|
|
||||||
|
|
||||||
String cacheKey = cacheKey(ownerFqn, methodName, receiverContext);
|
|
||||||
List<String> cached = sessionCache.get(cacheKey);
|
|
||||||
if (cached != null) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> resolved = resolveInternal(ownerFqn, methodName, receiverContext, budget, visited);
|
|
||||||
if (!resolved.isEmpty()) {
|
|
||||||
List<String> cachedCopy = List.copyOf(resolved);
|
|
||||||
sessionCache.put(cacheKey, cachedCopy);
|
|
||||||
return cachedCopy;
|
|
||||||
}
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GetterChainStep resolveGetterChainStep(String ownerFqn, String getterName, ResolutionBudget budget) {
|
|
||||||
if (ownerFqn == null || getterName == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(ownerFqn, getterName);
|
|
||||||
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter()) {
|
|
||||||
AccessorSummary accessor = indexedAccessor.get();
|
|
||||||
ClassInstanceCreation nextCic = fieldInitializerFinder.findFieldInitializerCic(ownerFqn, accessor.fieldName());
|
|
||||||
String nextType = fieldInitializerFinder.resolveFieldTypeName(ownerFqn, accessor.fieldName());
|
|
||||||
if (nextCic != null) {
|
|
||||||
nextType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(nextCic.getType());
|
|
||||||
}
|
|
||||||
if (nextType != null) {
|
|
||||||
return new GetterChainStep(nextType, nextCic);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String simpleType = simplifyTypeName(ownerFqn);
|
|
||||||
ClassInstanceCreation nextCic = constructorAnalyzer.findReturnedCIC(simpleType, getterName, context);
|
|
||||||
if (nextCic == null) {
|
|
||||||
String fieldName = indexedFieldName(ownerFqn, getterName);
|
|
||||||
nextCic = fieldInitializerFinder.findFieldInitializerCic(ownerFqn, fieldName);
|
|
||||||
}
|
|
||||||
if (nextCic != null) {
|
|
||||||
return new GetterChainStep(
|
|
||||||
click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(nextCic.getType()),
|
|
||||||
nextCic);
|
|
||||||
}
|
|
||||||
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(simpleType);
|
|
||||||
if (td == null) {
|
|
||||||
td = context.getTypeDeclaration(ownerFqn);
|
|
||||||
}
|
|
||||||
if (td != null) {
|
|
||||||
MethodDeclaration getter = context.findMethodDeclaration(td, getterName, true);
|
|
||||||
if (getter != null && getter.getReturnType2() != null) {
|
|
||||||
return new GetterChainStep(getter.getReturnType2().toString(), null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearSessionCache() {
|
|
||||||
sessionCache.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> resolveInternal(
|
|
||||||
String ownerFqn,
|
|
||||||
String methodName,
|
|
||||||
ReceiverContext receiverContext,
|
|
||||||
ResolutionBudget budget,
|
|
||||||
Set<String> visited) {
|
|
||||||
TypeDeclaration ownerType = receiverContext != null ? receiverContext.ownerType() : null;
|
|
||||||
if (ownerType == null) {
|
|
||||||
ownerType = context.getTypeDeclaration(ownerFqn);
|
|
||||||
}
|
|
||||||
boolean widenToImplementations = ownerType == null
|
|
||||||
|| ownerType.isInterface()
|
|
||||||
|| org.eclipse.jdt.core.dom.Modifier.isAbstract(ownerType.getModifiers());
|
|
||||||
|
|
||||||
if (widenToImplementations) {
|
|
||||||
List<String> widened = new ArrayList<>();
|
|
||||||
List<String> impls = context.getImplementations(ownerFqn);
|
|
||||||
if (impls != null) {
|
|
||||||
for (String implFqn : impls) {
|
|
||||||
TypeDeclaration implType = context.getTypeDeclaration(implFqn);
|
|
||||||
widened.addAll(resolve(
|
|
||||||
implFqn,
|
|
||||||
methodName,
|
|
||||||
new ReceiverContext(implType, null,
|
|
||||||
receiverContext != null ? receiverContext.contextCu() : null,
|
|
||||||
false,
|
|
||||||
receiverContext != null ? receiverContext.fieldValues() : null),
|
|
||||||
budget,
|
|
||||||
visited));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!widened.isEmpty()) {
|
|
||||||
return deduplicatePreservingOrder(widened);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(ownerFqn, methodName);
|
|
||||||
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter()) {
|
|
||||||
List<String> indexed = resolveIndexedAccessor(indexedAccessor.get(), receiverContext, visited);
|
|
||||||
if (!indexed.isEmpty()) {
|
|
||||||
return indexed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (receiverContext != null && receiverContext.ownerType() != null && receiverContext.cic() != null) {
|
|
||||||
List<String> cicValues = resolveWithCic(
|
|
||||||
ownerFqn, methodName, receiverContext, indexedAccessor.orElse(null), visited);
|
|
||||||
if (!cicValues.isEmpty()) {
|
|
||||||
return cicValues;
|
|
||||||
}
|
|
||||||
} else if (receiverContext != null && receiverContext.cic() != null) {
|
|
||||||
ReceiverContext enriched = new ReceiverContext(
|
|
||||||
context.getTypeDeclaration(ownerFqn),
|
|
||||||
receiverContext.cic(),
|
|
||||||
receiverContext.contextCu(),
|
|
||||||
receiverContext.anonymousGetter(),
|
|
||||||
receiverContext.fieldValues());
|
|
||||||
List<String> cicValues = resolveWithCic(
|
|
||||||
ownerFqn, methodName, enriched, indexedAccessor.orElse(null), visited);
|
|
||||||
if (!cicValues.isEmpty()) {
|
|
||||||
return cicValues;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (constantExtractor != null && !budget.isMethodReturnExhausted(0)) {
|
|
||||||
CompilationUnit contextCu = receiverContext != null ? receiverContext.contextCu() : null;
|
|
||||||
List<String> deep = constantExtractor.resolveMethodReturnConstant(
|
|
||||||
ownerFqn, methodName, 0, visited, contextCu, budget);
|
|
||||||
if (!deep.isEmpty()) {
|
|
||||||
return deep;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> resolveIndexedAccessor(
|
|
||||||
AccessorSummary accessor,
|
|
||||||
ReceiverContext receiverContext,
|
|
||||||
Set<String> visited) {
|
|
||||||
if (accessor.kind() == AccessorKind.CONSTANT_GETTER) {
|
|
||||||
List<String> constants = extractConstantGetterReturn(accessor.ownerFqn(), accessor.methodName());
|
|
||||||
if (!constants.isEmpty()) {
|
|
||||||
return constants;
|
|
||||||
}
|
|
||||||
if (constantExtractor != null) {
|
|
||||||
CompilationUnit contextCu = receiverContext != null ? receiverContext.contextCu() : null;
|
|
||||||
return constantExtractor.resolveMethodReturnConstant(
|
|
||||||
accessor.ownerFqn(), accessor.methodName(), 0, new HashSet<>(visited), contextCu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (constantExtractor != null) {
|
|
||||||
CompilationUnit contextCu = receiverContext != null ? receiverContext.contextCu() : null;
|
|
||||||
List<String> fieldConstants = constantExtractor.resolveIndexedGetterFieldConstants(
|
|
||||||
accessor, contextCu, visited);
|
|
||||||
if (!fieldConstants.isEmpty()) {
|
|
||||||
return fieldConstants;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (receiverContext != null
|
|
||||||
&& receiverContext.fieldValues() != null
|
|
||||||
&& accessor.fieldName() != null) {
|
|
||||||
String fieldValue = receiverContext.fieldValues().get(accessor.fieldName());
|
|
||||||
if (fieldValue != null) {
|
|
||||||
return List.of(fieldValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> resolveWithCic(
|
|
||||||
String ownerFqn,
|
|
||||||
String methodName,
|
|
||||||
ReceiverContext receiverContext,
|
|
||||||
AccessorSummary indexedAccessor,
|
|
||||||
Set<String> visited) {
|
|
||||||
TypeDeclaration td = receiverContext.ownerType();
|
|
||||||
if (td == null) {
|
|
||||||
td = context.getTypeDeclaration(ownerFqn);
|
|
||||||
}
|
|
||||||
ClassInstanceCreation cic = receiverContext.cic();
|
|
||||||
Map<String, String> fieldValues = receiverContext.fieldValues() != null
|
|
||||||
? receiverContext.fieldValues()
|
|
||||||
: new HashMap<>();
|
|
||||||
|
|
||||||
MethodDeclaration getter = null;
|
|
||||||
if (cic.getAnonymousClassDeclaration() != null) {
|
|
||||||
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
|
|
||||||
if (declObj instanceof MethodDeclaration md && md.getName().getIdentifier().equals(methodName)) {
|
|
||||||
getter = md;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (getter == null) {
|
|
||||||
getter = context.findMethodDeclaration(td, methodName, true);
|
|
||||||
}
|
|
||||||
if (getter != null && getter.getBody() != null) {
|
|
||||||
if (fieldValues.isEmpty() && constructorAnalyzer != null) {
|
|
||||||
fieldValues.putAll(constructorAnalyzer.buildFieldValuesFromCIC(td, cic, context, null));
|
|
||||||
}
|
|
||||||
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, visited);
|
|
||||||
if (result != null) {
|
|
||||||
return List.of(qualifyEnumLikeResult(getter, result));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (indexedAccessor != null && indexedAccessor.isGetter() && !receiverContext.anonymousGetter()) {
|
|
||||||
if (indexedAccessor.kind() == AccessorKind.CONSTANT_GETTER) {
|
|
||||||
List<String> constantValues = extractConstantGetterReturn(ownerFqn, methodName);
|
|
||||||
if (!constantValues.isEmpty()) {
|
|
||||||
return constantValues;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (fieldValues.isEmpty() && constructorAnalyzer != null) {
|
|
||||||
fieldValues.putAll(constructorAnalyzer.buildFieldValuesFromCIC(td, cic, context, null));
|
|
||||||
}
|
|
||||||
if (indexedAccessor.fieldName() != null) {
|
|
||||||
String indexedFieldValue = fieldValues.get(indexedAccessor.fieldName());
|
|
||||||
if (indexedFieldValue != null) {
|
|
||||||
return List.of(indexedFieldValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (constantExtractor != null) {
|
|
||||||
List<String> indexedConstants = constantExtractor.resolveIndexedGetterFieldConstants(
|
|
||||||
indexedAccessor, receiverContext.contextCu(), visited);
|
|
||||||
if (!indexedConstants.isEmpty()) {
|
|
||||||
return indexedConstants;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> extractConstantGetterReturn(String ownerFqn, String methodName) {
|
|
||||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(ownerFqn);
|
|
||||||
if (typeDeclaration == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
MethodDeclaration methodDeclaration = context.findMethodDeclaration(typeDeclaration, methodName, true);
|
|
||||||
if (methodDeclaration == null || methodDeclaration.getBody() == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
List<String> constants = new ArrayList<>();
|
|
||||||
BiConsumer<org.eclipse.jdt.core.dom.Expression, List<String>> extractor =
|
|
||||||
constantExtractor != null
|
|
||||||
? constantExtractor::extractConstantsFromExpression
|
|
||||||
: (expr, out) -> {
|
|
||||||
String resolved = constantResolver.resolve(expr, context);
|
|
||||||
if (resolved != null) {
|
|
||||||
out.add(resolved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
methodDeclaration.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(ReturnStatement node) {
|
|
||||||
if (node.getExpression() != null) {
|
|
||||||
extractor.accept(node.getExpression(), constants);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return constants;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String qualifyEnumLikeResult(MethodDeclaration getter, String result) {
|
|
||||||
if (result.contains(".")) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
String returnType = getter.getReturnType2() != null ? getter.getReturnType2().toString() : null;
|
|
||||||
if (returnType == null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
int lastDotRet = returnType.lastIndexOf('.');
|
|
||||||
String simpleReturnType = lastDotRet > 0 ? returnType.substring(lastDotRet + 1) : returnType;
|
|
||||||
if ("String".equals(simpleReturnType) || "Integer".equals(simpleReturnType)
|
|
||||||
|| "Long".equals(simpleReturnType) || "Boolean".equals(simpleReturnType)) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return simpleReturnType + "." + result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String indexedFieldName(String ownerFqn, String getterName) {
|
|
||||||
Optional<AccessorSummary> accessorOpt = lookupIndexedGetter(ownerFqn, getterName);
|
|
||||||
if (accessorOpt.isEmpty() || accessorOpt.get().fieldName() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return accessorOpt.get().fieldName();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<String> deduplicatePreservingOrder(List<String> values) {
|
|
||||||
List<String> deduped = new ArrayList<>();
|
|
||||||
for (String value : values) {
|
|
||||||
if (!deduped.contains(value)) {
|
|
||||||
deduped.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return deduped;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String cacheKey(String ownerFqn, String methodName, ReceiverContext receiverContext) {
|
|
||||||
String cicKey = receiverContext != null && receiverContext.cic() != null
|
|
||||||
? receiverContext.cic().toString()
|
|
||||||
: "";
|
|
||||||
String fieldValuesKey = receiverContext != null && receiverContext.fieldValues() != null
|
|
||||||
? receiverContext.fieldValues().toString()
|
|
||||||
: "";
|
|
||||||
return ownerFqn + "#" + methodName + "#" + cicKey + "#" + fieldValuesKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String simplifyTypeName(String typeFqn) {
|
|
||||||
if (typeFqn == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String simple = typeFqn.contains(".") ? typeFqn.substring(typeFqn.lastIndexOf('.') + 1) : typeFqn;
|
|
||||||
if (simple.contains("<")) {
|
|
||||||
simple = simple.substring(0, simple.indexOf('<'));
|
|
||||||
}
|
|
||||||
return simple;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
|
|
||||||
import org.eclipse.jdt.core.dom.FieldDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds {@code new ...()} field initializers, walking the inheritance chain once.
|
|
||||||
*/
|
|
||||||
public final class FieldInitializerFinder {
|
|
||||||
|
|
||||||
private final CodebaseContext context;
|
|
||||||
|
|
||||||
public FieldInitializerFinder(CodebaseContext context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClassInstanceCreation findFieldInitializerCic(String ownerFqn, String fieldName) {
|
|
||||||
if (fieldName == null || ownerFqn == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(ownerFqn);
|
|
||||||
return findFieldInitializerCic(td, fieldName, new HashSet<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClassInstanceCreation findFieldInitializerCic(TypeDeclaration td, String fieldName, Set<String> visited) {
|
|
||||||
if (td == null || fieldName == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String typeFqn = context.getFqn(td);
|
|
||||||
if (typeFqn == null || !visited.add(typeFqn)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (FieldDeclaration fd : td.getFields()) {
|
|
||||||
for (Object fragObj : fd.fragments()) {
|
|
||||||
if (fragObj instanceof VariableDeclarationFragment frag
|
|
||||||
&& frag.getName().getIdentifier().equals(fieldName)
|
|
||||||
&& frag.getInitializer() instanceof ClassInstanceCreation cic) {
|
|
||||||
return cic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String superFqn = context.getSuperclassFqn(td);
|
|
||||||
if (superFqn != null) {
|
|
||||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
|
||||||
ClassInstanceCreation inherited = findFieldInitializerCic(superTd, fieldName, visited);
|
|
||||||
if (inherited != null) {
|
|
||||||
return inherited;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String resolveFieldTypeName(String ownerFqn, String fieldName) {
|
|
||||||
if (ownerFqn == null || fieldName == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(ownerFqn);
|
|
||||||
return resolveFieldTypeName(td, fieldName, new HashSet<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String resolveFieldTypeName(TypeDeclaration td, String fieldName, Set<String> visited) {
|
|
||||||
if (td == null || fieldName == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String typeFqn = context.getFqn(td);
|
|
||||||
if (typeFqn == null || !visited.add(typeFqn)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (FieldDeclaration fd : td.getFields()) {
|
|
||||||
for (Object fragObj : fd.fragments()) {
|
|
||||||
if (fragObj instanceof VariableDeclarationFragment frag
|
|
||||||
&& frag.getName().getIdentifier().equals(fieldName)) {
|
|
||||||
return fd.getType().toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String superFqn = context.getSuperclassFqn(td);
|
|
||||||
if (superFqn != null) {
|
|
||||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
|
||||||
return resolveFieldTypeName(superTd, fieldName, visited);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Central registry for reactive/factory unwrap and passthrough method names used during constant extraction.
|
|
||||||
*/
|
|
||||||
public final class LibraryUnwrapRegistry {
|
|
||||||
|
|
||||||
private static final Set<String> UNWRAP_RECEIVER_TYPES = Set.of(
|
|
||||||
"Optional", "Stream", "List", "Set", "Arrays", "Objects", "Mono", "Flux");
|
|
||||||
|
|
||||||
private static final Set<String> UNWRAP_METHOD_NAMES = Set.of(
|
|
||||||
"of", "ofEntries", "asList", "entry", "just", "withPayload", "success");
|
|
||||||
|
|
||||||
private static final String[] STOP_UNWRAP_PREFIXES = {
|
|
||||||
"map", "parse", "convert", "to", "resolve", "build", "create", "from",
|
|
||||||
"transform", "translate", "derive", "determine", "calculate", "decode",
|
|
||||||
"extract", "get", "find", "validate"
|
|
||||||
};
|
|
||||||
|
|
||||||
private LibraryUnwrapRegistry() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isUnwrapReceiverType(String receiverExpressionText) {
|
|
||||||
return receiverExpressionText != null && UNWRAP_RECEIVER_TYPES.contains(receiverExpressionText);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isUnwrapMethodName(String methodName) {
|
|
||||||
return methodName != null && UNWRAP_METHOD_NAMES.contains(methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean shouldStopUnwrap(String methodName) {
|
|
||||||
if (methodName == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
String lowerName = methodName.toLowerCase(Locale.ROOT);
|
|
||||||
for (String prefix : STOP_UNWRAP_PREFIXES) {
|
|
||||||
if (lowerName.startsWith(prefix)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isEventSetterMethodName(String methodName) {
|
|
||||||
if (methodName == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return "event".equalsIgnoreCase(methodName)
|
|
||||||
|| "type".equalsIgnoreCase(methodName)
|
|
||||||
|| "eventType".equalsIgnoreCase(methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isHeaderSetterMethodName(String methodName) {
|
|
||||||
return "setHeader".equals(methodName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
|
||||||
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
|
|
||||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
|
||||||
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared {@link MethodInvocation} peeling for factory/reactive wrappers and local passthrough methods.
|
|
||||||
*/
|
|
||||||
public final class MethodInvocationUnwrapper {
|
|
||||||
|
|
||||||
private MethodInvocationUnwrapper() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Expression unwrap(
|
|
||||||
MethodInvocation mi,
|
|
||||||
int depth,
|
|
||||||
String methodFqn,
|
|
||||||
CodebaseContext context,
|
|
||||||
Function<MethodInvocation, String> targetMethodFqnResolver) {
|
|
||||||
if (ResolutionBudget.defaults().isUnwrapExhausted(depth)) {
|
|
||||||
return mi;
|
|
||||||
}
|
|
||||||
|
|
||||||
String targetMethodFqn = targetMethodFqnResolver != null
|
|
||||||
? targetMethodFqnResolver.apply(mi)
|
|
||||||
: null;
|
|
||||||
if (targetMethodFqn == null && methodFqn != null && methodFqn.contains(".") && mi.getExpression() == null) {
|
|
||||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
|
||||||
targetMethodFqn = className + "." + mi.getName().getIdentifier();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetMethodFqn != null && targetMethodFqn.contains(".")) {
|
|
||||||
String className = targetMethodFqn.substring(0, targetMethodFqn.lastIndexOf('.'));
|
|
||||||
String methodName = targetMethodFqn.substring(targetMethodFqn.lastIndexOf('.') + 1);
|
|
||||||
|
|
||||||
TypeDeclaration currentTd = methodFqn != null && methodFqn.contains(".")
|
|
||||||
? context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')))
|
|
||||||
: null;
|
|
||||||
org.eclipse.jdt.core.dom.CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof org.eclipse.jdt.core.dom.CompilationUnit compilationUnit
|
|
||||||
? compilationUnit
|
|
||||||
: null;
|
|
||||||
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(className, cu);
|
|
||||||
if (td == null) {
|
|
||||||
td = context.getTypeDeclaration(className);
|
|
||||||
}
|
|
||||||
if (td != null) {
|
|
||||||
MethodDeclaration localMd = context.findMethodDeclaration(td, methodName, true);
|
|
||||||
if (localMd != null) {
|
|
||||||
int paramIdx = getReturnedParameterIndex(localMd);
|
|
||||||
if (paramIdx >= 0 && paramIdx < mi.arguments().size()) {
|
|
||||||
Expression arg = peelExpression((Expression) mi.arguments().get(paramIdx));
|
|
||||||
if (arg instanceof MethodInvocation innerMi) {
|
|
||||||
return unwrap(innerMi, depth + 1, methodFqn, context, targetMethodFqnResolver);
|
|
||||||
}
|
|
||||||
return arg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mi.getExpression() != null) {
|
|
||||||
String exprStr = mi.getExpression().toString();
|
|
||||||
if (!LibraryUnwrapRegistry.isUnwrapReceiverType(exprStr)) {
|
|
||||||
return mi;
|
|
||||||
}
|
|
||||||
} else if (targetMethodFqn == null) {
|
|
||||||
return mi;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mi.arguments().isEmpty()) {
|
|
||||||
String methodName = mi.getName().getIdentifier();
|
|
||||||
if (LibraryUnwrapRegistry.shouldStopUnwrap(methodName)) {
|
|
||||||
return mi;
|
|
||||||
}
|
|
||||||
|
|
||||||
Expression arg = peelExpression((Expression) mi.arguments().get(0));
|
|
||||||
if (arg instanceof MethodInvocation innerMi) {
|
|
||||||
return unwrap(innerMi, depth + 1, methodFqn, context, targetMethodFqnResolver);
|
|
||||||
}
|
|
||||||
return arg;
|
|
||||||
}
|
|
||||||
return mi;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Expression peelExpression(Expression expression) {
|
|
||||||
Expression current = expression;
|
|
||||||
while (current instanceof ParenthesizedExpression pe) {
|
|
||||||
current = pe.getExpression();
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getReturnedParameterIndex(MethodDeclaration md) {
|
|
||||||
if (md.getBody() == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
List<?> parameters = md.parameters();
|
|
||||||
if (parameters.isEmpty()) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<String> returnedExprs = new ArrayList<>();
|
|
||||||
md.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(ReturnStatement node) {
|
|
||||||
if (node.getExpression() != null) {
|
|
||||||
returnedExprs.add(node.getExpression().toString());
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (returnedExprs.size() == 1) {
|
|
||||||
String retStr = returnedExprs.get(0);
|
|
||||||
for (int i = 0; i < parameters.size(); i++) {
|
|
||||||
if (parameters.get(i) instanceof SingleVariableDeclaration svd
|
|
||||||
&& svd.getName().getIdentifier().equals(retStr)) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared depth and fallback budget for analysis traversals. Replaces scattered magic-number limits.
|
|
||||||
*/
|
|
||||||
public final class ResolutionBudget {
|
|
||||||
|
|
||||||
public static final int DEFAULT_METHOD_RETURN = 20;
|
|
||||||
public static final int DEFAULT_ACCESSOR_INLINE = 25;
|
|
||||||
public static final int DEFAULT_DATAFLOW = 50;
|
|
||||||
public static final int DEFAULT_UNWRAP = 5;
|
|
||||||
public static final int DEFAULT_CONSTRUCTOR = 10;
|
|
||||||
public static final int DEFAULT_RETURNED_CIC = 50;
|
|
||||||
public static final int DEFAULT_GLOBAL_FIELD_SCAN = 10;
|
|
||||||
|
|
||||||
private final int methodReturnDepth;
|
|
||||||
private final int accessorInlineDepth;
|
|
||||||
private final int dataflowDepth;
|
|
||||||
private final int unwrapDepth;
|
|
||||||
private final int constructorDepth;
|
|
||||||
private final int returnedCicDepth;
|
|
||||||
private final int globalFieldScanLimit;
|
|
||||||
private final boolean allowGlobalFieldScan;
|
|
||||||
|
|
||||||
private ResolutionBudget(
|
|
||||||
int methodReturnDepth,
|
|
||||||
int accessorInlineDepth,
|
|
||||||
int dataflowDepth,
|
|
||||||
int unwrapDepth,
|
|
||||||
int constructorDepth,
|
|
||||||
int returnedCicDepth,
|
|
||||||
int globalFieldScanLimit,
|
|
||||||
boolean allowGlobalFieldScan) {
|
|
||||||
this.methodReturnDepth = methodReturnDepth;
|
|
||||||
this.accessorInlineDepth = accessorInlineDepth;
|
|
||||||
this.dataflowDepth = dataflowDepth;
|
|
||||||
this.unwrapDepth = unwrapDepth;
|
|
||||||
this.constructorDepth = constructorDepth;
|
|
||||||
this.returnedCicDepth = returnedCicDepth;
|
|
||||||
this.globalFieldScanLimit = globalFieldScanLimit;
|
|
||||||
this.allowGlobalFieldScan = allowGlobalFieldScan;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ResolutionBudget defaults() {
|
|
||||||
return new ResolutionBudget(
|
|
||||||
DEFAULT_METHOD_RETURN,
|
|
||||||
DEFAULT_ACCESSOR_INLINE,
|
|
||||||
DEFAULT_DATAFLOW,
|
|
||||||
DEFAULT_UNWRAP,
|
|
||||||
DEFAULT_CONSTRUCTOR,
|
|
||||||
DEFAULT_RETURNED_CIC,
|
|
||||||
DEFAULT_GLOBAL_FIELD_SCAN,
|
|
||||||
false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ResolutionBudget withGlobalFieldScan() {
|
|
||||||
return defaults().enableGlobalFieldScan();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ResolutionBudget enableGlobalFieldScan() {
|
|
||||||
return new ResolutionBudget(
|
|
||||||
methodReturnDepth,
|
|
||||||
accessorInlineDepth,
|
|
||||||
dataflowDepth,
|
|
||||||
unwrapDepth,
|
|
||||||
constructorDepth,
|
|
||||||
returnedCicDepth,
|
|
||||||
globalFieldScanLimit,
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ResolutionBudget child() {
|
|
||||||
return new ResolutionBudget(
|
|
||||||
methodReturnDepth - 1,
|
|
||||||
accessorInlineDepth - 1,
|
|
||||||
dataflowDepth - 1,
|
|
||||||
unwrapDepth,
|
|
||||||
constructorDepth - 1,
|
|
||||||
returnedCicDepth - 1,
|
|
||||||
globalFieldScanLimit,
|
|
||||||
allowGlobalFieldScan);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isMethodReturnExhausted(int depth) {
|
|
||||||
return depth > methodReturnDepth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAccessorInlineExhausted(int depth) {
|
|
||||||
return depth > accessorInlineDepth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDataflowExhausted(int depth) {
|
|
||||||
return depth > dataflowDepth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isUnwrapExhausted(int depth) {
|
|
||||||
return depth > unwrapDepth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isConstructorExhausted(int depth) {
|
|
||||||
return depth > constructorDepth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isReturnedCicExhausted(int depth) {
|
|
||||||
return depth > returnedCicDepth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int globalFieldScanLimit() {
|
|
||||||
return globalFieldScanLimit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean allowGlobalFieldScan() {
|
|
||||||
return allowGlobalFieldScan;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared boolean simplification for transition guards and dispatcher domain checks.
|
|
||||||
*/
|
|
||||||
public final class BooleanConstraintEvaluator {
|
|
||||||
|
|
||||||
private static final Pattern STRING_LITERAL_PATTERN = Pattern.compile("[\"']([a-zA-Z0-9_-]+)[\"']");
|
|
||||||
|
|
||||||
private BooleanConstraintEvaluator() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simplifies constraints such as {@code "ORDER".equals(domain)} against a machine domain key.
|
|
||||||
*/
|
|
||||||
public static boolean isCompatibleWithMachineDomain(String constraint, String machineDomainKey) {
|
|
||||||
if (constraint == null || machineDomainKey == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String cleanMachine = machineDomainKey.toUpperCase();
|
|
||||||
String expr = constraint;
|
|
||||||
Set<String> literals = extractStringLiterals(constraint);
|
|
||||||
|
|
||||||
for (String literal : literals) {
|
|
||||||
boolean matchesMachine = literal.equalsIgnoreCase(cleanMachine)
|
|
||||||
|| cleanMachine.equalsIgnoreCase(literal)
|
|
||||||
|| (cleanMachine.length() > literal.length() && cleanMachine.startsWith(literal.toUpperCase()));
|
|
||||||
String replacement = matchesMachine ? "true" : "false";
|
|
||||||
|
|
||||||
String regex1 = "(?i)[\"']?" + Pattern.quote(literal) + "[\"']?\\s*\\.\\s*(equalsIgnoreCase|equals)\\s*\\([^)]*\\)";
|
|
||||||
String regex2 = "(?i)[a-zA-Z0-9._]+\\s*\\.\\s*(equalsIgnoreCase|equals)\\s*\\(\\s*[\"']?"
|
|
||||||
+ Pattern.quote(literal) + "[\"']?\\s*\\)";
|
|
||||||
String regex3 = "(?i)[a-zA-Z0-9._]+\\s*==\\s*[\"']?" + Pattern.quote(literal) + "[\"']?";
|
|
||||||
String regex4 = "(?i)[a-zA-Z0-9._]+\\s*!=\\s*[\"']?" + Pattern.quote(literal) + "[\"']?";
|
|
||||||
|
|
||||||
expr = expr.replaceAll(regex1, replacement);
|
|
||||||
expr = expr.replaceAll(regex2, replacement);
|
|
||||||
expr = expr.replaceAll(regex3, replacement);
|
|
||||||
expr = expr.replaceAll(regex4, matchesMachine ? "false" : "true");
|
|
||||||
}
|
|
||||||
|
|
||||||
return evaluateBooleanExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean evaluateBooleanExpression(String expression) {
|
|
||||||
if (expression == null || expression.isBlank()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return parseExpression(expression.replaceAll("\\s+", ""));
|
|
||||||
} catch (Exception e) {
|
|
||||||
return !expression.contains("false");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluates a constraint against known variable bindings gathered while walking a call path forward.
|
|
||||||
* Supports {@code var == VALUE} where VALUE may be a quoted string or enum constant; suffix matching
|
|
||||||
* is used so {@code command == ORDER_PAY} matches {@code DomainCommand.ORDER_PAY}.
|
|
||||||
*/
|
|
||||||
public static boolean isCompatibleWithBindings(String constraint, Map<String, String> bindings) {
|
|
||||||
if (constraint == null || constraint.isBlank()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (bindings == null || bindings.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
Set<String> equalityVars = extractEqualityVariables(constraint);
|
|
||||||
for (String varName : equalityVars) {
|
|
||||||
if (!bindings.containsKey(varName)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String expr = constraint;
|
|
||||||
for (Map.Entry<String, String> entry : bindings.entrySet()) {
|
|
||||||
expr = substituteVariableBindings(expr, entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
return evaluateBooleanExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Set<String> extractEqualityVariables(String constraint) {
|
|
||||||
Set<String> vars = new HashSet<>();
|
|
||||||
Pattern pattern = Pattern.compile("([a-zA-Z][\\w]*)\\s*==");
|
|
||||||
Matcher matcher = pattern.matcher(constraint);
|
|
||||||
while (matcher.find()) {
|
|
||||||
vars.add(matcher.group(1));
|
|
||||||
}
|
|
||||||
return vars;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String substituteVariableBindings(String expr, String varName, String boundValue) {
|
|
||||||
if (boundValue == null || boundValue.isBlank()) {
|
|
||||||
return expr;
|
|
||||||
}
|
|
||||||
String cleanValue = boundValue;
|
|
||||||
if (cleanValue.startsWith("\"") && cleanValue.endsWith("\"")) {
|
|
||||||
cleanValue = cleanValue.substring(1, cleanValue.length() - 1);
|
|
||||||
}
|
|
||||||
String suffix = cleanValue.contains(".") ? cleanValue.substring(cleanValue.lastIndexOf('.') + 1) : cleanValue;
|
|
||||||
|
|
||||||
Pattern eqPattern = Pattern.compile(
|
|
||||||
"(?i)" + Pattern.quote(varName) + "\\s*==\\s*([\\w.\"']+)");
|
|
||||||
Matcher matcher = eqPattern.matcher(expr);
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
while (matcher.find()) {
|
|
||||||
String rhs = matcher.group(1).replace("\"", "").replace("'", "");
|
|
||||||
boolean matches = cleanValue.equals(rhs)
|
|
||||||
|| cleanValue.endsWith("." + rhs)
|
|
||||||
|| suffix.equalsIgnoreCase(rhs)
|
|
||||||
|| cleanValue.equalsIgnoreCase(rhs);
|
|
||||||
matcher.appendReplacement(sb, matches ? "true" : "false");
|
|
||||||
}
|
|
||||||
matcher.appendTail(sb);
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns {@code true}/{@code false} when both sides are compile-time string literals; otherwise {@code null}.
|
|
||||||
*/
|
|
||||||
public static Boolean evaluateStringEquals(String leftLiteral, String rightLiteral, boolean ignoreCase) {
|
|
||||||
if (leftLiteral == null || rightLiteral == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return ignoreCase
|
|
||||||
? leftLiteral.equalsIgnoreCase(rightLiteral)
|
|
||||||
: leftLiteral.equals(rightLiteral);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Set<String> extractStringLiterals(String constraint) {
|
|
||||||
Set<String> literals = new HashSet<>();
|
|
||||||
Matcher matcher = STRING_LITERAL_PATTERN.matcher(constraint);
|
|
||||||
while (matcher.find()) {
|
|
||||||
literals.add(matcher.group(1));
|
|
||||||
}
|
|
||||||
return literals;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean parseExpression(String expression) {
|
|
||||||
if (expression.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int parenDepth = 0;
|
|
||||||
for (int i = expression.length() - 1; i >= 0; i--) {
|
|
||||||
char c = expression.charAt(i);
|
|
||||||
if (c == ')') {
|
|
||||||
parenDepth++;
|
|
||||||
} else if (c == '(') {
|
|
||||||
parenDepth--;
|
|
||||||
} else if (parenDepth == 0 && i > 0 && expression.charAt(i) == '|' && expression.charAt(i - 1) == '|') {
|
|
||||||
return parseExpression(expression.substring(0, i - 1)) || parseExpression(expression.substring(i + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parenDepth = 0;
|
|
||||||
for (int i = expression.length() - 1; i >= 0; i--) {
|
|
||||||
char c = expression.charAt(i);
|
|
||||||
if (c == ')') {
|
|
||||||
parenDepth++;
|
|
||||||
} else if (c == '(') {
|
|
||||||
parenDepth--;
|
|
||||||
} else if (parenDepth == 0 && i > 0 && expression.charAt(i) == '&' && expression.charAt(i - 1) == '&') {
|
|
||||||
return parseExpression(expression.substring(0, i - 1)) && parseExpression(expression.substring(i + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expression.startsWith("!")) {
|
|
||||||
return !parseExpression(expression.substring(1));
|
|
||||||
}
|
|
||||||
if (expression.startsWith("(") && expression.endsWith(")")) {
|
|
||||||
return parseExpression(expression.substring(1, expression.length() - 1));
|
|
||||||
}
|
|
||||||
if ("true".equalsIgnoreCase(expression)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if ("false".equalsIgnoreCase(expression)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.eclipse.jdt.core.dom.*;
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
@@ -19,10 +17,6 @@ public class ConstantResolver {
|
|||||||
return resolveInternal(expr, context, new HashSet<>());
|
return resolveInternal(expr, context, new HashSet<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<String> decodeMapLiteralValues(String encoded) {
|
|
||||||
return LiteralExpressionSupport.mapLiteralValues(encoded);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String evaluateSwitchWithParams(org.eclipse.jdt.core.dom.SwitchExpression se, java.util.Map<String, String> paramValues, CodebaseContext context) {
|
public String evaluateSwitchWithParams(org.eclipse.jdt.core.dom.SwitchExpression se, java.util.Map<String, String> paramValues, CodebaseContext context) {
|
||||||
return evaluateSwitchExpression(se, paramValues, context, new HashSet<>());
|
return evaluateSwitchExpression(se, paramValues, context, new HashSet<>());
|
||||||
}
|
}
|
||||||
@@ -41,25 +35,6 @@ public class ConstantResolver {
|
|||||||
if (expr instanceof ParenthesizedExpression pe) {
|
if (expr instanceof ParenthesizedExpression pe) {
|
||||||
return resolveInternal(pe.getExpression(), context, visited);
|
return resolveInternal(pe.getExpression(), context, visited);
|
||||||
}
|
}
|
||||||
if (expr instanceof ArrayAccess access) {
|
|
||||||
String indexed = LiteralExpressionSupport.resolveArrayAccess(
|
|
||||||
access, context, visited, (e, v) -> resolveInternal(e, context, v));
|
|
||||||
if (indexed != null) {
|
|
||||||
return indexed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (expr instanceof ArrayCreation creation && creation.getInitializer() != null) {
|
|
||||||
List<String> elements = extractArrayElementsFromCreation(creation, context, visited);
|
|
||||||
if (elements != null) {
|
|
||||||
return LiteralExpressionSupport.encodeArrayLiteral(elements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (expr instanceof ArrayInitializer initializer) {
|
|
||||||
List<String> elements = extractArrayElementsFromInitializer(initializer, context, visited);
|
|
||||||
if (elements != null) {
|
|
||||||
return LiteralExpressionSupport.encodeArrayLiteral(elements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (expr instanceof MethodInvocation mi && (mi.getName().getIdentifier().equals("requireNonNull") || mi.getName().getIdentifier().equals("notNull"))) {
|
if (expr instanceof MethodInvocation mi && (mi.getName().getIdentifier().equals("requireNonNull") || mi.getName().getIdentifier().equals("notNull"))) {
|
||||||
if (!mi.arguments().isEmpty()) {
|
if (!mi.arguments().isEmpty()) {
|
||||||
return resolveInternal((Expression) mi.arguments().get(0), context, visited);
|
return resolveInternal((Expression) mi.arguments().get(0), context, visited);
|
||||||
@@ -101,10 +76,6 @@ public class ConstantResolver {
|
|||||||
return val != null ? val : qn.toString();
|
return val != null ? val : qn.toString();
|
||||||
}
|
}
|
||||||
if (expr instanceof FieldAccess fa) {
|
if (expr instanceof FieldAccess fa) {
|
||||||
String staticField = resolveStaticFieldAccess(fa, context, visited);
|
|
||||||
if (staticField != null) {
|
|
||||||
return staticField;
|
|
||||||
}
|
|
||||||
return resolveManual(fa.getName(), context, visited);
|
return resolveManual(fa.getName(), context, visited);
|
||||||
}
|
}
|
||||||
if (expr instanceof SimpleName sn) {
|
if (expr instanceof SimpleName sn) {
|
||||||
@@ -113,26 +84,7 @@ public class ConstantResolver {
|
|||||||
|
|
||||||
|
|
||||||
if (expr instanceof MethodInvocation mi) {
|
if (expr instanceof MethodInvocation mi) {
|
||||||
String methodName = mi.getName().getIdentifier();
|
if ((mi.getName().getIdentifier().equals("name") || mi.getName().getIdentifier().equals("toString")) && mi.arguments().isEmpty()) {
|
||||||
if ("get".equals(methodName) || "getOrDefault".equals(methodName)) {
|
|
||||||
String mapValue = LiteralExpressionSupport.resolveMapGet(
|
|
||||||
mi, context, visited, (e, v) -> resolveInternal(e, context, v));
|
|
||||||
if (mapValue != null) {
|
|
||||||
return mapValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String transformed = LiteralExpressionSupport.resolveLiteralStringTransform(
|
|
||||||
mi, visited, (e, v) -> resolveInternal(e, context, v));
|
|
||||||
if (transformed != null) {
|
|
||||||
return transformed;
|
|
||||||
}
|
|
||||||
java.util.Map<String, String> mapEntries = LiteralExpressionSupport.extractMapEntries(
|
|
||||||
mi, context, visited, (e, v) -> resolveInternal(e, context, v));
|
|
||||||
if (mapEntries != null && isMapFactoryInvocation(mi)) {
|
|
||||||
return LiteralExpressionSupport.encodeMapLiteral(mapEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((methodName.equals("name") || methodName.equals("toString")) && mi.arguments().isEmpty()) {
|
|
||||||
String val = resolveInternal(mi.getExpression(), context, visited);
|
String val = resolveInternal(mi.getExpression(), context, visited);
|
||||||
if (val != null) {
|
if (val != null) {
|
||||||
// If it was resolved as a fully qualified enum string like "MyEvents.ORDER_PAID", strip it
|
// If it was resolved as a fully qualified enum string like "MyEvents.ORDER_PAID", strip it
|
||||||
@@ -719,50 +671,6 @@ public class ConstantResolver {
|
|||||||
return fields.isEmpty() ? null : fields;
|
return fields.isEmpty() ? null : fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> extractArrayElementsFromCreation(ArrayCreation creation, CodebaseContext context, Set<String> visited) {
|
|
||||||
if (creation.getInitializer() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
List<String> values = new ArrayList<>();
|
|
||||||
for (Object expressionObj : creation.getInitializer().expressions()) {
|
|
||||||
String value = resolveInternal((Expression) expressionObj, context, visited);
|
|
||||||
if (value == null || value.startsWith("ARRAY:") || value.startsWith("MAP:")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
values.add(value);
|
|
||||||
}
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> extractArrayElementsFromInitializer(ArrayInitializer initializer, CodebaseContext context, Set<String> visited) {
|
|
||||||
List<String> values = new ArrayList<>();
|
|
||||||
for (Object expressionObj : initializer.expressions()) {
|
|
||||||
String value = resolveInternal((Expression) expressionObj, context, visited);
|
|
||||||
if (value == null || value.startsWith("ARRAY:") || value.startsWith("MAP:")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
values.add(value);
|
|
||||||
}
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isMapFactoryInvocation(MethodInvocation mi) {
|
|
||||||
if (!"of".equals(mi.getName().getIdentifier()) && !"ofEntries".equals(mi.getName().getIdentifier())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Expression receiver = mi.getExpression();
|
|
||||||
if (receiver == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (receiver instanceof SimpleName sn) {
|
|
||||||
return "Map".equals(sn.getIdentifier());
|
|
||||||
}
|
|
||||||
if (receiver instanceof QualifiedName qn) {
|
|
||||||
return "Map".equals(qn.getName().getIdentifier());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveInfix(InfixExpression expr, CodebaseContext context, Set<String> visited) {
|
private String resolveInfix(InfixExpression expr, CodebaseContext context, Set<String> visited) {
|
||||||
if (expr.getOperator() != InfixExpression.Operator.PLUS) return null;
|
if (expr.getOperator() != InfixExpression.Operator.PLUS) return null;
|
||||||
|
|
||||||
@@ -813,18 +721,7 @@ public class ConstantResolver {
|
|||||||
if (td != null) {
|
if (td != null) {
|
||||||
String fqn = context.getFqn(td);
|
String fqn = context.getFqn(td);
|
||||||
String result = resolveFieldInType(td, sn.getIdentifier(), fqn, context, visited);
|
String result = resolveFieldInType(td, sn.getIdentifier(), fqn, context, visited);
|
||||||
if (result != null) {
|
if (result != null) return result;
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
td = typeFromCurrentCallPath(context);
|
|
||||||
if (td != null) {
|
|
||||||
String fqn = context.getFqn(td);
|
|
||||||
String result = resolveFieldInType(td, sn.getIdentifier(), fqn, context, visited);
|
|
||||||
if (result != null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: Static imports
|
// Fallback: Static imports
|
||||||
@@ -845,9 +742,6 @@ public class ConstantResolver {
|
|||||||
|
|
||||||
CompilationUnit contextCu = (CompilationUnit) qn.getRoot();
|
CompilationUnit contextCu = (CompilationUnit) qn.getRoot();
|
||||||
TypeDeclaration td = context.getTypeDeclaration(qualifier, contextCu);
|
TypeDeclaration td = context.getTypeDeclaration(qualifier, contextCu);
|
||||||
if (td == null) {
|
|
||||||
td = resolveTypeInContext(qualifier, qn, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (td != null) {
|
if (td != null) {
|
||||||
String fqn = context.getFqn(td);
|
String fqn = context.getFqn(td);
|
||||||
@@ -857,67 +751,6 @@ public class ConstantResolver {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveStaticFieldAccess(FieldAccess fa, CodebaseContext context, Set<String> visited) {
|
|
||||||
Expression receiver = fa.getExpression();
|
|
||||||
if (receiver instanceof ThisExpression) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String typeName = null;
|
|
||||||
if (receiver instanceof SimpleName sn) {
|
|
||||||
typeName = sn.getIdentifier();
|
|
||||||
} else if (receiver instanceof QualifiedName qn) {
|
|
||||||
typeName = qn.getFullyQualifiedName();
|
|
||||||
}
|
|
||||||
if (typeName == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
TypeDeclaration td = resolveTypeInContext(typeName, fa, context);
|
|
||||||
if (td == null) {
|
|
||||||
td = typeFromCurrentCallPath(context);
|
|
||||||
}
|
|
||||||
if (td == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return resolveFieldInType(td, fa.getName().getIdentifier(), context.getFqn(td), context, visited);
|
|
||||||
}
|
|
||||||
|
|
||||||
private TypeDeclaration resolveTypeInContext(String typeName, ASTNode node, CodebaseContext context) {
|
|
||||||
CompilationUnit contextCu = node.getRoot() instanceof CompilationUnit cu ? cu : null;
|
|
||||||
TypeDeclaration td = contextCu != null
|
|
||||||
? context.getTypeDeclaration(typeName, contextCu)
|
|
||||||
: context.getTypeDeclaration(typeName);
|
|
||||||
if (td != null) {
|
|
||||||
return td;
|
|
||||||
}
|
|
||||||
String entryClass = getCurrentCallPathEntryClass();
|
|
||||||
if (entryClass == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
TypeDeclaration entryTd = context.getTypeDeclaration(entryClass);
|
|
||||||
if (entryTd == null || !(entryTd.getRoot() instanceof CompilationUnit entryCu)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return context.getTypeDeclaration(typeName, entryCu);
|
|
||||||
}
|
|
||||||
|
|
||||||
private TypeDeclaration typeFromCurrentCallPath(CodebaseContext context) {
|
|
||||||
String entryClass = getCurrentCallPathEntryClass();
|
|
||||||
if (entryClass == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return context.getTypeDeclaration(entryClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getCurrentCallPathEntryClass() {
|
|
||||||
List<String> path = JdtDataFlowModel.getCurrentPath();
|
|
||||||
if (path == null || path.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String entryMethod = path.get(0);
|
|
||||||
int dot = entryMethod.lastIndexOf('.');
|
|
||||||
return dot > 0 ? entryMethod.substring(0, dot) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveFieldInType(TypeDeclaration td, String fieldName, String typeFqn, CodebaseContext context, Set<String> visited) {
|
private String resolveFieldInType(TypeDeclaration td, String fieldName, String typeFqn, CodebaseContext context, Set<String> visited) {
|
||||||
String fieldId = typeFqn + "#" + fieldName;
|
String fieldId = typeFqn + "#" + fieldName;
|
||||||
if (visited.contains(fieldId)) {
|
if (visited.contains(fieldId)) {
|
||||||
@@ -1037,19 +870,9 @@ public class ConstantResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (valueExpr instanceof StringLiteral sl) {
|
if (valueExpr instanceof StringLiteral sl) {
|
||||||
String literal = sl.getLiteralValue();
|
return sl.getLiteralValue();
|
||||||
if (literal != null && literal.contains("${")) {
|
|
||||||
return click.kamil.springstatemachineexporter.analysis.util.PlaceholderResolver.resolve(
|
|
||||||
literal, java.util.Collections.emptyMap());
|
|
||||||
}
|
|
||||||
return literal;
|
|
||||||
}
|
}
|
||||||
String raw = valueExpr != null ? valueExpr.toString().replaceAll("^\"|\"$", "") : null;
|
return valueExpr != null ? valueExpr.toString().replaceAll("^\"|\"$", "") : null;
|
||||||
if (raw != null && raw.contains("${")) {
|
|
||||||
return click.kamil.springstatemachineexporter.analysis.util.PlaceholderResolver.resolve(
|
|
||||||
raw, java.util.Collections.emptyMap());
|
|
||||||
}
|
|
||||||
return raw;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Discovers generated {@code .java} sources under Maven {@code target/} and Gradle {@code build/} when
|
|
||||||
* they already exist in the workspace. Does not run Maven or Gradle.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public final class GeneratedSourceDiscovery {
|
|
||||||
|
|
||||||
private static final List<String> BUILD_ROOTS = List.of("target", "build");
|
|
||||||
|
|
||||||
private static final List<String> EXCLUDED_RELATIVE_SEGMENTS = List.of(
|
|
||||||
"classes",
|
|
||||||
"test-classes",
|
|
||||||
"generated-test-sources",
|
|
||||||
"generated-test",
|
|
||||||
"test",
|
|
||||||
"testfixtures",
|
|
||||||
"test-fixtures",
|
|
||||||
"androidtest",
|
|
||||||
"inttest",
|
|
||||||
"functionaltest",
|
|
||||||
"e2etest",
|
|
||||||
"libs",
|
|
||||||
"lib",
|
|
||||||
"tmp",
|
|
||||||
"reports",
|
|
||||||
"test-results",
|
|
||||||
"surefire",
|
|
||||||
"failsafe",
|
|
||||||
"surefire-reports",
|
|
||||||
"failsafe-reports",
|
|
||||||
"cache",
|
|
||||||
"caches",
|
|
||||||
"outputs",
|
|
||||||
"intermediates",
|
|
||||||
"kotlin",
|
|
||||||
"native",
|
|
||||||
"resources",
|
|
||||||
"distributions",
|
|
||||||
"docs",
|
|
||||||
"javadoc",
|
|
||||||
"testOutput");
|
|
||||||
|
|
||||||
public Set<Path> discoverSourceRoots(Collection<Path> moduleDirectories) {
|
|
||||||
Set<Path> roots = new LinkedHashSet<>();
|
|
||||||
if (moduleDirectories == null) {
|
|
||||||
return roots;
|
|
||||||
}
|
|
||||||
for (Path moduleDir : moduleDirectories) {
|
|
||||||
if (moduleDir == null || !Files.isDirectory(moduleDir)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
roots.addAll(discoverSourceRoots(moduleDir));
|
|
||||||
}
|
|
||||||
return roots;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<Path> discoverSourceRoots(Path moduleDirectory) {
|
|
||||||
Set<Path> roots = new LinkedHashSet<>();
|
|
||||||
Path normalizedModule = moduleDirectory.toAbsolutePath().normalize();
|
|
||||||
for (String buildRootName : BUILD_ROOTS) {
|
|
||||||
Path buildRoot = normalizedModule.resolve(buildRootName);
|
|
||||||
if (containsGeneratedJava(normalizedModule, buildRoot)) {
|
|
||||||
roots.add(buildRoot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!roots.isEmpty()) {
|
|
||||||
log.info("Discovered generated source roots under {}: {}", normalizedModule, roots);
|
|
||||||
}
|
|
||||||
return roots;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasGeneratedSources(Path moduleDirectory) {
|
|
||||||
return !discoverSourceRoots(moduleDirectory).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean containsGeneratedJava(Path moduleDirectory, Path buildRoot) {
|
|
||||||
if (!Files.isDirectory(buildRoot)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try (Stream<Path> files = Files.walk(buildRoot)) {
|
|
||||||
return files.filter(Files::isRegularFile)
|
|
||||||
.anyMatch(path -> path.getFileName().toString().endsWith(".java")
|
|
||||||
&& !isExcludedBuildPath(moduleDirectory, path));
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.debug("Failed to scan generated sources under {}", buildRoot, e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isExcludedBuildPath(Path moduleDirectory, Path candidate) {
|
|
||||||
Path normalizedModule = moduleDirectory.toAbsolutePath().normalize();
|
|
||||||
Path normalizedCandidate = candidate.toAbsolutePath().normalize();
|
|
||||||
if (!normalizedCandidate.startsWith(normalizedModule)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
Path relative = normalizedModule.relativize(normalizedCandidate);
|
|
||||||
if (relative.getNameCount() < 2) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String buildRoot = relative.getName(0).toString().toLowerCase(Locale.ROOT);
|
|
||||||
if (!buildRoot.equals("target") && !buildRoot.equals("build")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
for (int i = 1; i < relative.getNameCount(); i++) {
|
|
||||||
String segment = relative.getName(i).toString().toLowerCase(Locale.ROOT);
|
|
||||||
if (isExcludedBuildSegment(segment)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isExcludedBuildSegment(String segment) {
|
|
||||||
if (segment == null || segment.isBlank()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String lower = segment.toLowerCase(Locale.ROOT);
|
|
||||||
if (EXCLUDED_RELATIVE_SEGMENTS.contains(lower)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return lower.contains("test-sources")
|
|
||||||
|| lower.startsWith("test-")
|
|
||||||
|| lower.endsWith("-test")
|
|
||||||
|| lower.contains("-test-");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.*;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.BiFunction;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compile-time literal folding for map lookups, array indexing, and narrow string transforms.
|
|
||||||
*/
|
|
||||||
final class LiteralExpressionSupport {
|
|
||||||
|
|
||||||
private LiteralExpressionSupport() {
|
|
||||||
}
|
|
||||||
|
|
||||||
static String resolveArrayAccess(
|
|
||||||
ArrayAccess access,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
Integer index = resolveLiteralInt(access.getIndex(), context, visited, resolver);
|
|
||||||
if (index == null || index < 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
List<String> elements = extractArrayElements(access.getArray(), context, visited, resolver);
|
|
||||||
if (elements == null || index >= elements.size()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return elements.get(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String resolveMapGet(
|
|
||||||
MethodInvocation getCall,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
if (getCall.arguments().isEmpty() || getCall.getExpression() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String key = resolver.apply((Expression) getCall.arguments().get(0), visited);
|
|
||||||
if (key == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Map<String, String> entries = extractMapEntries(getCall.getExpression(), context, visited, resolver);
|
|
||||||
if (entries == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return entries.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String resolveLiteralStringTransform(
|
|
||||||
MethodInvocation transformCall,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
if (!transformCall.arguments().isEmpty() || transformCall.getExpression() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String receiver = resolver.apply(transformCall.getExpression(), visited);
|
|
||||||
if (receiver == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return switch (transformCall.getName().getIdentifier()) {
|
|
||||||
case "toUpperCase" -> receiver.toUpperCase(Locale.ROOT);
|
|
||||||
case "toLowerCase" -> receiver.toLowerCase(Locale.ROOT);
|
|
||||||
case "trim" -> receiver.trim();
|
|
||||||
default -> null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static Integer resolveLiteralInt(
|
|
||||||
Expression expr,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
if (expr instanceof NumberLiteral nl) {
|
|
||||||
try {
|
|
||||||
return Integer.parseInt(nl.getToken().replace("_", ""));
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (expr instanceof ParenthesizedExpression pe) {
|
|
||||||
return resolveLiteralInt(pe.getExpression(), context, visited, resolver);
|
|
||||||
}
|
|
||||||
if (expr instanceof InfixExpression infix) {
|
|
||||||
Integer left = resolveLiteralInt(infix.getLeftOperand(), context, visited, resolver);
|
|
||||||
Integer right = resolveLiteralInt(infix.getRightOperand(), context, visited, resolver);
|
|
||||||
if (left == null || right == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
InfixExpression.Operator op = infix.getOperator();
|
|
||||||
if (op == InfixExpression.Operator.PLUS) {
|
|
||||||
return left + right;
|
|
||||||
}
|
|
||||||
if (op == InfixExpression.Operator.MINUS) {
|
|
||||||
return left - right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String resolved = resolver.apply(expr, visited);
|
|
||||||
if (resolved != null) {
|
|
||||||
try {
|
|
||||||
return Integer.parseInt(resolved.replace("_", ""));
|
|
||||||
} catch (NumberFormatException ignored) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, String> extractMapEntries(
|
|
||||||
Expression expr,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
if (expr instanceof MethodInvocation mi) {
|
|
||||||
Map<String, String> fromFactory = extractMapFactoryEntries(mi, context, visited, resolver);
|
|
||||||
if (fromFactory != null) {
|
|
||||||
return fromFactory;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String resolvedField = resolver.apply(expr, visited);
|
|
||||||
if (resolvedField != null && resolvedField.startsWith("MAP:")) {
|
|
||||||
return parseMapLiteral(resolvedField);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, String> extractMapFactoryEntries(
|
|
||||||
MethodInvocation mi,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
if (!isMapFactory(mi.getExpression())) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String method = mi.getName().getIdentifier();
|
|
||||||
if ("of".equals(method)) {
|
|
||||||
return extractAlternatingPairs(mi.arguments(), context, visited, resolver);
|
|
||||||
}
|
|
||||||
if ("ofEntries".equals(method)) {
|
|
||||||
Map<String, String> entries = new LinkedHashMap<>();
|
|
||||||
for (Object argObj : mi.arguments()) {
|
|
||||||
if (argObj instanceof Expression entryExpr) {
|
|
||||||
Map<String, String> single = extractMapEntryCall(entryExpr, context, visited, resolver);
|
|
||||||
if (single != null) {
|
|
||||||
entries.putAll(single);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return entries.isEmpty() ? null : entries;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, String> extractMapEntryCall(
|
|
||||||
Expression expr,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
if (!(expr instanceof MethodInvocation entryCall) || !"entry".equals(entryCall.getName().getIdentifier())) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!isMapFactory(entryCall.getExpression()) || entryCall.arguments().size() != 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String key = resolver.apply((Expression) entryCall.arguments().get(0), visited);
|
|
||||||
String value = resolver.apply((Expression) entryCall.arguments().get(1), visited);
|
|
||||||
if (key == null || value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return Map.of(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, String> extractAlternatingPairs(
|
|
||||||
List<?> arguments,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
if (arguments.size() < 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Map<String, String> entries = new LinkedHashMap<>();
|
|
||||||
for (int i = 0; i + 1 < arguments.size(); i += 2) {
|
|
||||||
String key = resolver.apply((Expression) arguments.get(i), visited);
|
|
||||||
String value = resolver.apply((Expression) arguments.get(i + 1), visited);
|
|
||||||
if (key == null || value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
entries.put(key, value);
|
|
||||||
}
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
static String encodeMapLiteral(Map<String, String> entries) {
|
|
||||||
StringBuilder sb = new StringBuilder("MAP:");
|
|
||||||
boolean first = true;
|
|
||||||
for (Map.Entry<String, String> entry : entries.entrySet()) {
|
|
||||||
if (!first) {
|
|
||||||
sb.append('|');
|
|
||||||
}
|
|
||||||
first = false;
|
|
||||||
sb.append(escape(entry.getKey())).append('=').append(escape(entry.getValue()));
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, String> parseMapLiteral(String encoded) {
|
|
||||||
if (encoded == null || !encoded.startsWith("MAP:")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Map<String, String> entries = new LinkedHashMap<>();
|
|
||||||
for (String pair : encoded.substring(4).split("\\|")) {
|
|
||||||
int eq = pair.indexOf('=');
|
|
||||||
if (eq <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
entries.put(unescape(pair.substring(0, eq)), unescape(pair.substring(eq + 1)));
|
|
||||||
}
|
|
||||||
return entries.isEmpty() ? null : entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<String> mapLiteralValues(String encoded) {
|
|
||||||
Map<String, String> entries = parseMapLiteral(encoded);
|
|
||||||
if (entries == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return List.copyOf(entries.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String escape(String value) {
|
|
||||||
return value.replace("\\", "\\\\").replace("|", "\\|").replace("=", "\\=");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String unescape(String value) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (int i = 0; i < value.length(); i++) {
|
|
||||||
char c = value.charAt(i);
|
|
||||||
if (c == '\\' && i + 1 < value.length()) {
|
|
||||||
sb.append(value.charAt(++i));
|
|
||||||
} else {
|
|
||||||
sb.append(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<String> extractArrayElements(
|
|
||||||
Expression arrayExpr,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
if (arrayExpr instanceof ArrayCreation creation && creation.getInitializer() != null) {
|
|
||||||
return extractExpressionList(creation.getInitializer().expressions(), context, visited, resolver);
|
|
||||||
}
|
|
||||||
if (arrayExpr instanceof ArrayInitializer initializer) {
|
|
||||||
return extractExpressionList(initializer.expressions(), context, visited, resolver);
|
|
||||||
}
|
|
||||||
String resolved = resolver.apply(arrayExpr, visited);
|
|
||||||
if (resolved != null && resolved.startsWith("ARRAY:")) {
|
|
||||||
return List.of(resolved.substring(6).split("\\|", -1));
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static String encodeArrayLiteral(List<String> values) {
|
|
||||||
return "ARRAY:" + String.join("|", values.stream().map(LiteralExpressionSupport::escape).toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<String> extractExpressionList(
|
|
||||||
List<?> expressions,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
List<String> values = new ArrayList<>();
|
|
||||||
for (Object expressionObj : expressions) {
|
|
||||||
String value = resolver.apply((Expression) expressionObj, visited);
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
values.add(value);
|
|
||||||
}
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isMapFactory(Expression expression) {
|
|
||||||
if (expression == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (expression instanceof SimpleName sn) {
|
|
||||||
return "Map".equals(sn.getIdentifier());
|
|
||||||
}
|
|
||||||
if (expression instanceof QualifiedName qn) {
|
|
||||||
return "Map".equals(qn.getName().getIdentifier());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Event;
|
|
||||||
import click.kamil.springstatemachineexporter.model.State;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Canonicalizes enum-backed state/event identifiers for analysis using the owning machine config's
|
|
||||||
* {@code StateMachineConfigurerAdapter<State, Event>} type parameters.
|
|
||||||
*
|
|
||||||
* <p>Display formatting ({@code --event}/{@code --state}) is separate; this fixes internal
|
|
||||||
* {@code fullIdentifier} values used for matching and traceability.
|
|
||||||
*/
|
|
||||||
public final class MachineEnumCanonicalizer {
|
|
||||||
|
|
||||||
private MachineEnumCanonicalizer() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void canonicalizeTransitions(
|
|
||||||
List<Transition> transitions,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
|
||||||
if (transitions == null || machineTypes == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (Transition transition : transitions) {
|
|
||||||
if (transition.getEvent() != null) {
|
|
||||||
transition.setEvent(canonicalizeEvent(transition.getEvent(), machineTypes.eventTypeFqn()));
|
|
||||||
}
|
|
||||||
if (transition.getSourceStates() != null) {
|
|
||||||
transition.setSourceStates(canonicalizeStates(transition.getSourceStates(), machineTypes.stateTypeFqn()));
|
|
||||||
}
|
|
||||||
if (transition.getTargetStates() != null) {
|
|
||||||
transition.setTargetStates(canonicalizeStates(transition.getTargetStates(), machineTypes.stateTypeFqn()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Set<String> canonicalizeStateLabels(Set<String> labels, String stateTypeFqn) {
|
|
||||||
if (labels == null || labels.isEmpty()) {
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
return labels.stream()
|
|
||||||
.map(label -> canonicalizeLabel(label, stateTypeFqn))
|
|
||||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Set<State> canonicalizeStates(Set<State> states, String stateTypeFqn) {
|
|
||||||
if (states == null || states.isEmpty()) {
|
|
||||||
return states;
|
|
||||||
}
|
|
||||||
return states.stream()
|
|
||||||
.map(state -> canonicalizeState(state, stateTypeFqn))
|
|
||||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TriggerPoint canonicalizeTriggerPoint(
|
|
||||||
TriggerPoint trigger,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
|
||||||
if (trigger == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String eventTypeFqn = preferFullTypeFqn(
|
|
||||||
trigger.getEventTypeFqn(),
|
|
||||||
machineTypes != null ? machineTypes.eventTypeFqn() : null);
|
|
||||||
String stateTypeFqn = preferFullTypeFqn(
|
|
||||||
trigger.getStateTypeFqn(),
|
|
||||||
machineTypes != null ? machineTypes.stateTypeFqn() : null);
|
|
||||||
|
|
||||||
List<String> polymorphicEvents = trigger.getPolymorphicEvents() == null ? null
|
|
||||||
: trigger.getPolymorphicEvents().stream()
|
|
||||||
.map(event -> canonicalizeLabel(event, eventTypeFqn))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
return trigger.toBuilder()
|
|
||||||
.eventTypeFqn(eventTypeFqn)
|
|
||||||
.stateTypeFqn(stateTypeFqn)
|
|
||||||
.event(canonicalizeLabel(trigger.getEvent(), eventTypeFqn))
|
|
||||||
.sourceState(canonicalizeLabel(trigger.getSourceState(), stateTypeFqn))
|
|
||||||
.polymorphicEvents(polymorphicEvents)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String qualifyEventIdentifier(String event, String eventTypeFqn) {
|
|
||||||
if (event == null || eventTypeFqn == null || event.isEmpty()) {
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
if (event.startsWith("<SYMBOLIC: ")) {
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
if (isWildcardVariable(event)) {
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
if (Character.isLowerCase(event.charAt(0))) {
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
if (!isEnumConstantCandidate(event)) {
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
if (isStringOrPrimitiveType(eventTypeFqn)) {
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
if (isMachineEnumReference(event, eventTypeFqn)) {
|
|
||||||
return canonicalizeLabel(event, stripGenerics(eventTypeFqn));
|
|
||||||
}
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String preferFullTypeFqn(String primary, String fallback) {
|
|
||||||
if (primary != null && primary.contains(".")) {
|
|
||||||
return stripGenerics(primary);
|
|
||||||
}
|
|
||||||
if (fallback != null && fallback.contains(".")) {
|
|
||||||
return stripGenerics(fallback);
|
|
||||||
}
|
|
||||||
return primary != null ? primary : fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String stripGenerics(String typeFqn) {
|
|
||||||
int idx = typeFqn.indexOf('<');
|
|
||||||
return idx >= 0 ? typeFqn.substring(0, idx) : typeFqn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isStringOrPrimitiveType(String typeFqn) {
|
|
||||||
if (typeFqn == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return typeFqn.equals("String") || typeFqn.equals("java.lang.String")
|
|
||||||
|| typeFqn.equals("int") || typeFqn.equals("long") || typeFqn.equals("char")
|
|
||||||
|| typeFqn.equals("java.lang.Object") || typeFqn.equals("Object");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isEnumConstantCandidate(String eventStr) {
|
|
||||||
if (eventStr == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (eventStr.startsWith("<SYMBOLIC: ")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !eventStr.contains(" ") && !eventStr.contains("(") && !eventStr.contains(")")
|
|
||||||
&& !eventStr.contains("?");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isWildcardVariable(String eventStr) {
|
|
||||||
if (eventStr == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (eventStr.equals("event") || eventStr.equals("e")
|
|
||||||
|| eventStr.equals("msg") || eventStr.equals("message")
|
|
||||||
|| eventStr.equals("payload")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (eventStr.contains(".valueOf(") || eventStr.contains("valueOf (")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return eventStr.contains(" ? ") && eventStr.contains(" : ");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<State> canonicalizeStates(List<State> states, String stateTypeFqn) {
|
|
||||||
if (states == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
List<State> canonical = new ArrayList<>(states.size());
|
|
||||||
for (State state : states) {
|
|
||||||
canonical.add(canonicalizeState(state, stateTypeFqn));
|
|
||||||
}
|
|
||||||
return canonical;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Event canonicalizeEvent(Event event, String enumTypeFqn) {
|
|
||||||
if (event == null || enumTypeFqn == null || enumTypeFqn.isBlank()) {
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
String canonical = canonicalizeLabel(
|
|
||||||
event.fullIdentifier() != null ? event.fullIdentifier() : event.rawName(),
|
|
||||||
enumTypeFqn);
|
|
||||||
String fnRaw = toFnForm(canonical, enumTypeFqn);
|
|
||||||
if (fnRaw == null) {
|
|
||||||
fnRaw = event.rawName();
|
|
||||||
}
|
|
||||||
if (canonical.equals(event.fullIdentifier()) && fnRaw.equals(event.rawName())) {
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
return Event.of(fnRaw, canonical);
|
|
||||||
}
|
|
||||||
|
|
||||||
static State canonicalizeState(State state, String enumTypeFqn) {
|
|
||||||
if (state == null || enumTypeFqn == null || enumTypeFqn.isBlank()) {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
String raw = resolvePlaceholder(state.rawName());
|
|
||||||
String full = resolvePlaceholder(
|
|
||||||
state.fullIdentifier() != null ? state.fullIdentifier() : state.rawName());
|
|
||||||
String canonical = canonicalizeLabel(full, enumTypeFqn);
|
|
||||||
if (canonical.equals(state.fullIdentifier()) && raw.equals(state.rawName())) {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
if (isStringOrPrimitiveType(enumTypeFqn) && canonical.startsWith(enumTypeFqn + ".")) {
|
|
||||||
String constant = canonical.substring(enumTypeFqn.length() + 1);
|
|
||||||
raw = "\"" + constant + "\"";
|
|
||||||
} else {
|
|
||||||
String fnRaw = toFnForm(canonical, enumTypeFqn);
|
|
||||||
if (fnRaw != null) {
|
|
||||||
raw = fnRaw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return State.of(raw, canonical);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String resolvePlaceholder(String value) {
|
|
||||||
if (value == null || !value.contains("${")) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return click.kamil.springstatemachineexporter.analysis.util.PlaceholderResolver.resolve(
|
|
||||||
value, java.util.Collections.emptyMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isMachineEnumReference(String value, String enumTypeFqn) {
|
|
||||||
if (value == null || value.isBlank() || enumTypeFqn == null || enumTypeFqn.isBlank()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (isStringOrPrimitiveType(enumTypeFqn)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (value.startsWith("<SYMBOLIC: ")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (value.length() >= 2 && value.startsWith("\"")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!isEnumConstantCandidate(value)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (Character.isLowerCase(value.charAt(0))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String typePart = enumTypeFromRef(value);
|
|
||||||
if (typePart == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return enumTypesMatch(enumTypeFqn, typePart);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String canonicalizeLabel(String value, String enumTypeFqn) {
|
|
||||||
if (value == null || value.isBlank() || enumTypeFqn == null || enumTypeFqn.isBlank()) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
value = resolvePlaceholder(value);
|
|
||||||
if (value == null || value.isBlank()) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
|
|
||||||
return stripQuotes(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
String stripped = value;
|
|
||||||
if (stripped == null || stripped.isBlank()) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stripped.startsWith(enumTypeFqn + ".")) {
|
|
||||||
return stripped;
|
|
||||||
}
|
|
||||||
|
|
||||||
String constant = constantName(stripped);
|
|
||||||
String typePart = enumTypeFromRef(stripped);
|
|
||||||
|
|
||||||
if (typePart != null && enumTypesMatch(enumTypeFqn, typePart)) {
|
|
||||||
return enumTypeFqn + "." + constant;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typePart == null && Character.isUpperCase(stripped.charAt(0)) && !stripped.contains(".")) {
|
|
||||||
return enumTypeFqn + "." + stripped;
|
|
||||||
}
|
|
||||||
|
|
||||||
return stripped;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String stripQuotes(String value) {
|
|
||||||
if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
|
|
||||||
return value.substring(1, value.length() - 1);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String constantName(String ref) {
|
|
||||||
int dot = ref.lastIndexOf('.');
|
|
||||||
return dot >= 0 ? ref.substring(dot + 1) : ref;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String enumTypeFromRef(String ref) {
|
|
||||||
int dot = ref.lastIndexOf('.');
|
|
||||||
if (dot <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return ref.substring(0, dot);
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean enumTypesMatch(String type1, String type2) {
|
|
||||||
if (type1 == null || type2 == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (type1.equals(type2)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (type1.endsWith("." + type2) || type2.endsWith("." + type1)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
String simple1 = simpleName(type1);
|
|
||||||
String simple2 = simpleName(type2);
|
|
||||||
if (!simple1.equals(simple2)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !type1.contains(".") || !type2.contains(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String simpleName(String fqn) {
|
|
||||||
return fqn.contains(".") ? fqn.substring(fqn.lastIndexOf('.') + 1) : fqn;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Derives {@code EnumFormat.fn} display form ({@code OrderEvent.PAY}) from a package-canonical identifier.
|
|
||||||
*/
|
|
||||||
static String toFnForm(String canonicalFqn, String enumTypeFqn) {
|
|
||||||
if (canonicalFqn == null || canonicalFqn.isBlank() || isStringOrPrimitiveType(enumTypeFqn)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (canonicalFqn.startsWith("<") || canonicalFqn.startsWith("\"")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!canonicalFqn.contains(".")) {
|
|
||||||
return canonicalFqn;
|
|
||||||
}
|
|
||||||
int lastDot = canonicalFqn.lastIndexOf('.');
|
|
||||||
int prevDot = canonicalFqn.lastIndexOf('.', lastDot - 1);
|
|
||||||
if (prevDot > 0) {
|
|
||||||
return canonicalFqn.substring(prevDot + 1);
|
|
||||||
}
|
|
||||||
return simpleName(enumTypeFqn) + "." + canonicalFqn.substring(lastDot + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Static wiring model built from parsed {@code settings.gradle}, {@code build.gradle}, and {@code pom.xml}
|
|
||||||
* files — no Maven/Gradle execution.
|
|
||||||
*/
|
|
||||||
public final class ProjectModuleGraph {
|
|
||||||
|
|
||||||
public enum BuildSystem {
|
|
||||||
GRADLE,
|
|
||||||
MAVEN,
|
|
||||||
MIXED
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum DependencyKind {
|
|
||||||
LOCAL_PROJECT,
|
|
||||||
EXTERNAL_LIBRARY
|
|
||||||
}
|
|
||||||
|
|
||||||
public record MavenCoordinates(String groupId, String artifactId) {
|
|
||||||
public MavenCoordinates {
|
|
||||||
Objects.requireNonNull(groupId, "groupId");
|
|
||||||
Objects.requireNonNull(artifactId, "artifactId");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String key() {
|
|
||||||
return groupId + ":" + artifactId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record ProjectModule(
|
|
||||||
String id,
|
|
||||||
Path directory,
|
|
||||||
BuildSystem buildSystem,
|
|
||||||
MavenCoordinates mavenCoordinates) {
|
|
||||||
|
|
||||||
public Optional<MavenCoordinates> coordinates() {
|
|
||||||
return Optional.ofNullable(mavenCoordinates);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record ModuleDependency(
|
|
||||||
String fromModuleId,
|
|
||||||
String toModuleId,
|
|
||||||
MavenCoordinates externalCoordinates,
|
|
||||||
DependencyKind kind) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Path workspaceRoot;
|
|
||||||
private final Map<String, ProjectModule> modulesById;
|
|
||||||
private final Map<MavenCoordinates, ProjectModule> modulesByCoordinates;
|
|
||||||
private final List<ModuleDependency> dependencies;
|
|
||||||
|
|
||||||
public ProjectModuleGraph(
|
|
||||||
Path workspaceRoot,
|
|
||||||
Map<String, ProjectModule> modulesById,
|
|
||||||
Map<MavenCoordinates, ProjectModule> modulesByCoordinates,
|
|
||||||
List<ModuleDependency> dependencies) {
|
|
||||||
this.workspaceRoot = workspaceRoot != null ? workspaceRoot.normalize() : null;
|
|
||||||
this.modulesById = Map.copyOf(modulesById);
|
|
||||||
this.modulesByCoordinates = Map.copyOf(modulesByCoordinates);
|
|
||||||
this.dependencies = List.copyOf(dependencies);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Path workspaceRoot() {
|
|
||||||
return workspaceRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, ProjectModule> modulesById() {
|
|
||||||
return modulesById;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<MavenCoordinates, ProjectModule> modulesByCoordinates() {
|
|
||||||
return modulesByCoordinates;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<ModuleDependency> dependencies() {
|
|
||||||
return dependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<ProjectModule> findByDirectory(Path directory) {
|
|
||||||
if (directory == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Path normalized = directory.toAbsolutePath().normalize();
|
|
||||||
return modulesById.values().stream()
|
|
||||||
.filter(module -> module.directory().toAbsolutePath().normalize().equals(normalized))
|
|
||||||
.findFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<ProjectModule> findContainingModule(Path path) {
|
|
||||||
if (path == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Path normalized = path.toAbsolutePath().normalize();
|
|
||||||
ProjectModule best = null;
|
|
||||||
int bestLength = -1;
|
|
||||||
for (ProjectModule module : modulesById.values()) {
|
|
||||||
Path moduleDir = module.directory().toAbsolutePath().normalize();
|
|
||||||
if (normalized.startsWith(moduleDir) && moduleDir.getNameCount() > bestLength) {
|
|
||||||
best = module;
|
|
||||||
bestLength = moduleDir.getNameCount();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.ofNullable(best);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<Path> allModuleDirectories() {
|
|
||||||
Set<Path> paths = new LinkedHashSet<>();
|
|
||||||
for (ProjectModule module : modulesById.values()) {
|
|
||||||
if (Files.isDirectory(module.directory())) {
|
|
||||||
paths.add(module.directory().toAbsolutePath().normalize());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return paths;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isGradleAggregatorRoot(Path directory) {
|
|
||||||
if (directory == null || workspaceRoot == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return directory.toAbsolutePath().normalize().equals(workspaceRoot)
|
|
||||||
&& modulesById.values().stream().anyMatch(module -> module.buildSystem() == BuildSystem.GRADLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isMavenAggregatorRoot(Path directory) {
|
|
||||||
if (directory == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Path normalized = directory.toAbsolutePath().normalize();
|
|
||||||
long childModules = modulesById.values().stream()
|
|
||||||
.filter(module -> module.directory().toAbsolutePath().normalize().startsWith(normalized))
|
|
||||||
.filter(module -> !module.directory().toAbsolutePath().normalize().equals(normalized))
|
|
||||||
.count();
|
|
||||||
return childModules > 0 && findByDirectory(normalized)
|
|
||||||
.map(module -> module.buildSystem() == BuildSystem.MAVEN || module.buildSystem() == BuildSystem.MIXED)
|
|
||||||
.orElse(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves scan roots for analysis: the input directory, all Gradle includes when scanning from the
|
|
||||||
* workspace root, and transitively reachable local modules from build-file wiring.
|
|
||||||
*/
|
|
||||||
public Set<Path> resolveScanPaths(Path inputDir) {
|
|
||||||
return resolveScanPaths(inputDir, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<Path> resolveScanPaths(Path inputDir, boolean includeGeneratedSources) {
|
|
||||||
if (inputDir == null) {
|
|
||||||
return Set.of();
|
|
||||||
}
|
|
||||||
Set<Path> scanPaths = new LinkedHashSet<>();
|
|
||||||
Path normalizedInput = inputDir.toAbsolutePath().normalize();
|
|
||||||
scanPaths.add(normalizedInput);
|
|
||||||
|
|
||||||
if (workspaceRoot != null && normalizedInput.equals(workspaceRoot)) {
|
|
||||||
scanPaths.addAll(allModuleDirectories());
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<ProjectModule> inputModule = findContainingModule(normalizedInput);
|
|
||||||
if (inputModule.isPresent()) {
|
|
||||||
scanPaths.addAll(transitiveLocalModuleDirectories(inputModule.get().id()));
|
|
||||||
resolveMavenParentDirectory(inputModule.get().directory()).ifPresent(scanPaths::add);
|
|
||||||
Path parentDir = inputModule.get().directory().getParent();
|
|
||||||
if (parentDir != null && Files.exists(parentDir.resolve("pom.xml"))) {
|
|
||||||
scanPaths.add(parentDir.toAbsolutePath().normalize());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeGeneratedSources) {
|
|
||||||
scanPaths.addAll(new GeneratedSourceDiscovery().discoverSourceRoots(scanPaths));
|
|
||||||
}
|
|
||||||
return scanPaths;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Optional<Path> resolveMavenParentDirectory(Path moduleDirectory) {
|
|
||||||
if (moduleDirectory == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Path pom = moduleDirectory.resolve("pom.xml");
|
|
||||||
if (!Files.exists(pom)) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
String content = Files.readString(pom);
|
|
||||||
Matcher parentMatcher = Pattern.compile(
|
|
||||||
"<parent>.*?<relativePath>(.*?)</relativePath>.*?</parent>", Pattern.DOTALL).matcher(content);
|
|
||||||
if (!parentMatcher.find()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
String relPathStr = parentMatcher.group(1).trim();
|
|
||||||
Path relPath = moduleDirectory.resolve(relPathStr).normalize();
|
|
||||||
if (Files.isRegularFile(relPath) && "pom.xml".equals(relPath.getFileName().toString())) {
|
|
||||||
return Optional.of(relPath.getParent().toAbsolutePath().normalize());
|
|
||||||
}
|
|
||||||
if (Files.isDirectory(relPath)) {
|
|
||||||
return Optional.of(relPath.toAbsolutePath().normalize());
|
|
||||||
}
|
|
||||||
} catch (IOException ignored) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<Path> transitiveLocalModuleDirectories(String rootModuleId) {
|
|
||||||
Set<Path> reachable = new LinkedHashSet<>();
|
|
||||||
Queue<String> queue = new ArrayDeque<>();
|
|
||||||
Set<String> visited = new HashSet<>();
|
|
||||||
if (rootModuleId != null) {
|
|
||||||
queue.add(rootModuleId);
|
|
||||||
}
|
|
||||||
while (!queue.isEmpty()) {
|
|
||||||
String current = queue.poll();
|
|
||||||
if (!visited.add(current)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ProjectModule module = modulesById.get(current);
|
|
||||||
if (module != null && Files.isDirectory(module.directory())) {
|
|
||||||
reachable.add(module.directory().toAbsolutePath().normalize());
|
|
||||||
}
|
|
||||||
for (ModuleDependency dependency : dependencies) {
|
|
||||||
if (dependency.kind() == DependencyKind.LOCAL_PROJECT
|
|
||||||
&& current.equals(dependency.fromModuleId())
|
|
||||||
&& dependency.toModuleId() != null) {
|
|
||||||
queue.add(dependency.toModuleId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return reachable;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<ModuleDependency> localDependenciesOf(String moduleId) {
|
|
||||||
List<ModuleDependency> result = new ArrayList<>();
|
|
||||||
for (ModuleDependency dependency : dependencies) {
|
|
||||||
if (moduleId.equals(dependency.fromModuleId())) {
|
|
||||||
result.add(dependency);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,41 +3,224 @@ package click.kamil.springstatemachineexporter.analysis.resolver;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.*;
|
||||||
import java.util.Set;
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
|
||||||
* Discovers related local modules by statically parsing Maven/Gradle build files (never executes build tools).
|
|
||||||
*/
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class SiblingDependencyResolver {
|
public class SiblingDependencyResolver {
|
||||||
|
|
||||||
private final StaticBuildFileAnalyzer analyzer = new StaticBuildFileAnalyzer();
|
private static final Pattern GRADLE_PROJECT_DEP = Pattern.compile("project\\(['\"]:(.*?)['\"]\\)");
|
||||||
|
private static final Pattern MAVEN_DEPENDENCY = Pattern.compile("<dependency>\\s*<groupId>(.*?)</groupId>\\s*<artifactId>(.*?)</artifactId>\\s*<version>(.*?)</version>", Pattern.DOTALL);
|
||||||
public ProjectModuleGraph analyzeProject(Path inputDir) throws IOException {
|
|
||||||
return analyzer.analyze(inputDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<Path> resolveSiblings(Path projectDir) {
|
public Set<Path> resolveSiblings(Path projectDir) {
|
||||||
try {
|
Set<Path> allSiblings = new HashSet<>();
|
||||||
ProjectModuleGraph graph = analyzer.analyze(projectDir);
|
Queue<Path> queue = new LinkedList<>();
|
||||||
return resolveSiblings(graph, projectDir);
|
queue.add(projectDir);
|
||||||
} catch (IOException e) {
|
|
||||||
log.warn("Error resolving sibling dependencies for {}", projectDir, e);
|
while (!queue.isEmpty()) {
|
||||||
return Set.of();
|
Path current = queue.poll();
|
||||||
|
Set<Path> currentSiblings = new HashSet<>();
|
||||||
|
try {
|
||||||
|
currentSiblings.addAll(resolveGradleSiblings(current));
|
||||||
|
currentSiblings.addAll(resolveMavenSiblings(current));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error resolving sibling dependencies for {}", current, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Path sibling : currentSiblings) {
|
||||||
|
if (!allSiblings.contains(sibling) && !sibling.equals(projectDir)) {
|
||||||
|
allSiblings.add(sibling);
|
||||||
|
queue.add(sibling);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return allSiblings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<Path> resolveSiblings(ProjectModuleGraph graph, Path inputDir) {
|
private Set<Path> resolveGradleSiblings(Path projectDir) throws IOException {
|
||||||
Set<Path> scanPaths = graph.resolveScanPaths(inputDir);
|
Set<Path> siblings = new HashSet<>();
|
||||||
Set<Path> siblings = new LinkedHashSet<>();
|
Path buildGradle = projectDir.resolve("build.gradle");
|
||||||
Path normalizedInput = inputDir.toAbsolutePath().normalize();
|
if (!Files.exists(buildGradle)) {
|
||||||
for (Path path : scanPaths) {
|
buildGradle = projectDir.resolve("build.gradle.kts");
|
||||||
if (!path.equals(normalizedInput)) {
|
}
|
||||||
siblings.add(path);
|
|
||||||
|
if (Files.exists(buildGradle)) {
|
||||||
|
log.info("Found build.gradle at {}", buildGradle);
|
||||||
|
String content = Files.readString(buildGradle);
|
||||||
|
Matcher matcher = GRADLE_PROJECT_DEP.matcher(content);
|
||||||
|
Set<String> gradlePaths = new HashSet<>();
|
||||||
|
while (matcher.find()) {
|
||||||
|
gradlePaths.add(matcher.group(1));
|
||||||
|
}
|
||||||
|
log.info("Found project dependencies: {}", gradlePaths);
|
||||||
|
|
||||||
|
if (!gradlePaths.isEmpty()) {
|
||||||
|
Path rootDir = findGradleRoot(projectDir);
|
||||||
|
log.info("Resolved Gradle root to: {}", rootDir);
|
||||||
|
if (rootDir != null) {
|
||||||
|
for (String gradlePath : gradlePaths) {
|
||||||
|
String relativePath = gradlePath.replace(':', '/');
|
||||||
|
Path resolvedSibling = rootDir.resolve(relativePath).normalize();
|
||||||
|
|
||||||
|
// Check for explicit projectDir mapping in settings.gradle or settings.gradle.kts
|
||||||
|
Path settingsFile = rootDir.resolve("settings.gradle");
|
||||||
|
if (!Files.exists(settingsFile)) {
|
||||||
|
settingsFile = rootDir.resolve("settings.gradle.kts");
|
||||||
|
}
|
||||||
|
if (Files.exists(settingsFile)) {
|
||||||
|
String settingsContent = Files.readString(settingsFile);
|
||||||
|
String regex = "project\\(['\"]:" + gradlePath + "['\"]\\)\\.projectDir\\s*=\\s*(?:file\\(['\"](.*?)['\"]\\)|new File\\(.*?,\\s*['\"](.*?)['\"]\\))";
|
||||||
|
Matcher settingsMatcher = Pattern.compile(regex).matcher(settingsContent);
|
||||||
|
if (settingsMatcher.find()) {
|
||||||
|
String customPath = settingsMatcher.group(1) != null ? settingsMatcher.group(1) : settingsMatcher.group(2);
|
||||||
|
resolvedSibling = rootDir.resolve(customPath).normalize();
|
||||||
|
log.info("Found custom projectDir in settings for {}: {}", gradlePath, resolvedSibling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.exists(resolvedSibling)) {
|
||||||
|
log.info("Discovered local Gradle sibling dependency: {}", resolvedSibling);
|
||||||
|
siblings.add(resolvedSibling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return siblings;
|
return siblings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Path findGradleRoot(Path start) {
|
||||||
|
Path current = start.toAbsolutePath();
|
||||||
|
Path lastSettingsDir = null;
|
||||||
|
while (current != null) {
|
||||||
|
if (Files.exists(current.resolve("settings.gradle")) || Files.exists(current.resolve("settings.gradle.kts"))) {
|
||||||
|
lastSettingsDir = current;
|
||||||
|
}
|
||||||
|
if (Files.exists(current.resolve(".git"))) {
|
||||||
|
return lastSettingsDir != null ? lastSettingsDir : current;
|
||||||
|
}
|
||||||
|
current = current.getParent();
|
||||||
|
}
|
||||||
|
return lastSettingsDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Path> resolveMavenSiblings(Path projectDir) throws IOException {
|
||||||
|
Set<Path> siblings = new HashSet<>();
|
||||||
|
Path pom = projectDir.resolve("pom.xml");
|
||||||
|
if (Files.exists(pom)) {
|
||||||
|
log.info("Found pom.xml at {}", pom);
|
||||||
|
String content = Files.readString(pom);
|
||||||
|
|
||||||
|
Set<String> targetArtifacts = new HashSet<>();
|
||||||
|
Matcher matcher = Pattern.compile("<artifactId>(.*?)</artifactId>").matcher(content);
|
||||||
|
// We want artifacts from the <dependencies> section
|
||||||
|
int depsStart = content.indexOf("<dependencies>");
|
||||||
|
int depsEnd = content.indexOf("</dependencies>");
|
||||||
|
if (depsStart != -1 && depsEnd > depsStart) {
|
||||||
|
String depsContent = content.substring(depsStart, depsEnd);
|
||||||
|
Matcher depMatcher = Pattern.compile("<artifactId>(.*?)</artifactId>").matcher(depsContent);
|
||||||
|
while (depMatcher.find()) {
|
||||||
|
targetArtifacts.add(depMatcher.group(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("Found maven target artifacts: {}", targetArtifacts);
|
||||||
|
|
||||||
|
if (!targetArtifacts.isEmpty()) {
|
||||||
|
Path rootDir = findMavenRoot(projectDir);
|
||||||
|
log.info("Resolved Maven root to: {}", rootDir);
|
||||||
|
if (rootDir != null) {
|
||||||
|
try (Stream<Path> paths = Files.walk(rootDir)) {
|
||||||
|
List<Path> allPoms = paths.filter(p -> p.getFileName().toString().equals("pom.xml"))
|
||||||
|
.filter(p -> !p.toString().contains("/target/"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
log.info("Scanned {} total pom.xml files under {}", allPoms.size(), rootDir);
|
||||||
|
|
||||||
|
for (Path p : allPoms) {
|
||||||
|
try {
|
||||||
|
String pContent = Files.readString(p);
|
||||||
|
String artifactId = extractOwnArtifactId(pContent);
|
||||||
|
log.info("Checking artifactId {} in {}", artifactId, p);
|
||||||
|
if (artifactId != null && targetArtifacts.contains(artifactId)) {
|
||||||
|
Path siblingDir = p.getParent();
|
||||||
|
if (!siblingDir.equals(projectDir)) {
|
||||||
|
log.info("Discovered local Maven sibling dependency via artifact match: {}", siblingDir);
|
||||||
|
siblings.add(siblingDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also directly add aggregator <module> directories declared in this pom
|
||||||
|
Matcher moduleMatcher = Pattern.compile("<module>(.*?)</module>").matcher(content);
|
||||||
|
while (moduleMatcher.find()) {
|
||||||
|
String moduleName = moduleMatcher.group(1).trim();
|
||||||
|
Path moduleDir = projectDir.resolve(moduleName).normalize();
|
||||||
|
if (Files.exists(moduleDir) && Files.exists(moduleDir.resolve("pom.xml")) && !moduleDir.equals(projectDir)) {
|
||||||
|
log.info("Discovered local Maven sibling dependency via <module>: {}", moduleDir);
|
||||||
|
siblings.add(moduleDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also add parent dir if it has a pom.xml
|
||||||
|
Path parentDir = projectDir.getParent();
|
||||||
|
|
||||||
|
// Check for explicit <relativePath> in parent block
|
||||||
|
Matcher parentRelativePathMatcher = Pattern.compile("<parent>.*?<relativePath>(.*?)</relativePath>.*?</parent>", Pattern.DOTALL).matcher(content);
|
||||||
|
if (parentRelativePathMatcher.find()) {
|
||||||
|
String relPathStr = parentRelativePathMatcher.group(1).trim();
|
||||||
|
Path relPath = projectDir.resolve(relPathStr).normalize();
|
||||||
|
if (Files.isRegularFile(relPath) && relPath.getFileName().toString().equals("pom.xml")) {
|
||||||
|
parentDir = relPath.getParent();
|
||||||
|
} else if (Files.isDirectory(relPath)) {
|
||||||
|
parentDir = relPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentDir != null && Files.exists(parentDir.resolve("pom.xml")) && !parentDir.equals(projectDir)) {
|
||||||
|
log.info("Discovered local Maven parent sibling: {}", parentDir);
|
||||||
|
siblings.add(parentDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return siblings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractOwnArtifactId(String pomContent) {
|
||||||
|
// Strip <parent> block if it exists
|
||||||
|
String content = pomContent;
|
||||||
|
int parentEnd = pomContent.indexOf("</parent>");
|
||||||
|
if (parentEnd != -1) {
|
||||||
|
content = pomContent.substring(parentEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher matcher = Pattern.compile("<artifactId>(.*?)</artifactId>").matcher(content);
|
||||||
|
if (matcher.find()) {
|
||||||
|
return matcher.group(1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path findMavenRoot(Path start) {
|
||||||
|
Path current = start.toAbsolutePath();
|
||||||
|
Path lastPomDir = null;
|
||||||
|
while (current != null) {
|
||||||
|
if (Files.exists(current.resolve("pom.xml"))) {
|
||||||
|
lastPomDir = current;
|
||||||
|
}
|
||||||
|
if (Files.exists(current.resolve(".git"))) {
|
||||||
|
return lastPomDir != null ? lastPomDir : current;
|
||||||
|
}
|
||||||
|
current = current.getParent();
|
||||||
|
}
|
||||||
|
return lastPomDir;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,254 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.*;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves {@code StateMachineConfigurerAdapter<S, E>} type arguments for a config class.
|
|
||||||
* Used by analysis (canonical enum ids) and routing (machine scoping).
|
|
||||||
*/
|
|
||||||
public final class StateMachineTypeResolver {
|
|
||||||
|
|
||||||
public record MachineTypes(String stateTypeFqn, String eventTypeFqn) {
|
|
||||||
public static MachineTypes empty() {
|
|
||||||
return new MachineTypes(null, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private StateMachineTypeResolver() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String[] resolve(String machineConfigFqn, CodebaseContext context) {
|
|
||||||
MachineTypes types = resolveTypes(machineConfigFqn, context);
|
|
||||||
return new String[]{types.stateTypeFqn(), types.eventTypeFqn()};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MachineTypes resolveTypes(String machineConfigFqn, CodebaseContext context) {
|
|
||||||
if (machineConfigFqn == null || context == null) {
|
|
||||||
return MachineTypes.empty();
|
|
||||||
}
|
|
||||||
String configFqn = machineConfigFqn;
|
|
||||||
if (configFqn.contains("#")) {
|
|
||||||
configFqn = configFqn.substring(0, configFqn.indexOf('#'));
|
|
||||||
}
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(configFqn);
|
|
||||||
if (td == null) {
|
|
||||||
return MachineTypes.empty();
|
|
||||||
}
|
|
||||||
Set<String> visited = new HashSet<>();
|
|
||||||
visited.add(configFqn);
|
|
||||||
String[] args = findTypeArgumentsRecursively(
|
|
||||||
td.getSuperclassType(),
|
|
||||||
context,
|
|
||||||
(CompilationUnit) td.getRoot(),
|
|
||||||
visited,
|
|
||||||
Map.of());
|
|
||||||
return new MachineTypes(args[0], args[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean extendsEnumStateMachineConfigurer(String machineConfigFqn, CodebaseContext context) {
|
|
||||||
if (machineConfigFqn == null || context == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String configFqn = machineConfigFqn;
|
|
||||||
if (configFqn.contains("#")) {
|
|
||||||
configFqn = configFqn.substring(0, configFqn.indexOf('#'));
|
|
||||||
}
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(configFqn);
|
|
||||||
if (td == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Set<String> visited = new HashSet<>();
|
|
||||||
visited.add(configFqn);
|
|
||||||
return hierarchyHasEnumConfigurer(
|
|
||||||
td.getSuperclassType(),
|
|
||||||
context,
|
|
||||||
(CompilationUnit) td.getRoot(),
|
|
||||||
visited);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean hierarchyHasEnumConfigurer(
|
|
||||||
Type type,
|
|
||||||
CodebaseContext context,
|
|
||||||
CompilationUnit cu,
|
|
||||||
Set<String> visited) {
|
|
||||||
if (type == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (type instanceof ParameterizedType pt) {
|
|
||||||
String rawTypeName = pt.getType().toString();
|
|
||||||
if (rawTypeName.equals("EnumStateMachineConfigurerAdapter")
|
|
||||||
|| rawTypeName.endsWith(".EnumStateMachineConfigurerAdapter")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(rawTypeName, cu);
|
|
||||||
if (td != null) {
|
|
||||||
String fqn = context.getFqn(td);
|
|
||||||
if (visited.add(fqn)) {
|
|
||||||
Type superclass = td instanceof TypeDeclaration typeDecl ? typeDecl.getSuperclassType() : null;
|
|
||||||
if (hierarchyHasEnumConfigurer(superclass, context, cu, visited)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
String simpleName = type.toString();
|
|
||||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
|
||||||
if (td != null) {
|
|
||||||
String fqn = context.getFqn(td);
|
|
||||||
if (visited.add(fqn)) {
|
|
||||||
Type superclass = td instanceof TypeDeclaration typeDecl ? typeDecl.getSuperclassType() : null;
|
|
||||||
if (hierarchyHasEnumConfigurer(superclass, context, cu, visited)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String[] findTypeArgumentsRecursively(
|
|
||||||
Type type,
|
|
||||||
CodebaseContext context,
|
|
||||||
CompilationUnit cu,
|
|
||||||
Set<String> visited,
|
|
||||||
Map<String, String> typeVarBindings) {
|
|
||||||
if (type == null) {
|
|
||||||
return new String[]{null, null};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type instanceof ParameterizedType pt) {
|
|
||||||
String rawTypeName = pt.getType().toString();
|
|
||||||
List<?> typeArgs = pt.typeArguments();
|
|
||||||
|
|
||||||
if (isStateMachineConfigurerAdapter(rawTypeName)) {
|
|
||||||
if (typeArgs.size() >= 2) {
|
|
||||||
String stateType = resolveTypeToFqn((Type) typeArgs.get(0), cu, context, typeVarBindings);
|
|
||||||
String eventType = resolveTypeToFqn((Type) typeArgs.get(1), cu, context, typeVarBindings);
|
|
||||||
return new String[]{stateType, eventType};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, String> nextBindings = extendBindings(rawTypeName, typeArgs, cu, context, typeVarBindings);
|
|
||||||
|
|
||||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(rawTypeName, cu);
|
|
||||||
if (td != null) {
|
|
||||||
String fqn = context.getFqn(td);
|
|
||||||
if (visited.add(fqn)) {
|
|
||||||
Type superclass = td instanceof TypeDeclaration typeDecl ? typeDecl.getSuperclassType() : null;
|
|
||||||
String[] res = findTypeArgumentsRecursively(superclass, context, cu, visited, nextBindings);
|
|
||||||
if (res[0] != null || res[1] != null) {
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
String simpleName = type.toString();
|
|
||||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
|
||||||
if (td != null) {
|
|
||||||
String fqn = context.getFqn(td);
|
|
||||||
if (visited.add(fqn)) {
|
|
||||||
Type superclass = td instanceof TypeDeclaration typeDecl ? typeDecl.getSuperclassType() : null;
|
|
||||||
String[] res = findTypeArgumentsRecursively(superclass, context, cu, visited, typeVarBindings);
|
|
||||||
if (res[0] != null || res[1] != null) {
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new String[]{null, null};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, String> extendBindings(
|
|
||||||
String rawTypeName,
|
|
||||||
List<?> typeArgs,
|
|
||||||
CompilationUnit cu,
|
|
||||||
CodebaseContext context,
|
|
||||||
Map<String, String> parentBindings) {
|
|
||||||
Map<String, String> bindings = new HashMap<>(parentBindings);
|
|
||||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(rawTypeName, cu);
|
|
||||||
if (!(td instanceof TypeDeclaration typeDecl) || typeArgs.isEmpty()) {
|
|
||||||
return bindings;
|
|
||||||
}
|
|
||||||
List<TypeParameter> typeParameters = typeDecl.typeParameters();
|
|
||||||
for (int i = 0; i < Math.min(typeParameters.size(), typeArgs.size()); i++) {
|
|
||||||
String paramName = typeParameters.get(i).getName().getIdentifier();
|
|
||||||
String resolvedArg = resolveTypeToFqn((Type) typeArgs.get(i), cu, context, parentBindings);
|
|
||||||
if (resolvedArg != null) {
|
|
||||||
bindings.put(paramName, resolvedArg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bindings;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isStateMachineConfigurerAdapter(String rawTypeName) {
|
|
||||||
return rawTypeName.equals("StateMachineConfigurerAdapter")
|
|
||||||
|| rawTypeName.equals("EnumStateMachineConfigurerAdapter")
|
|
||||||
|| rawTypeName.endsWith(".StateMachineConfigurerAdapter")
|
|
||||||
|| rawTypeName.endsWith(".EnumStateMachineConfigurerAdapter");
|
|
||||||
}
|
|
||||||
|
|
||||||
static String resolveTypeToFqn(Type type, CompilationUnit cu, CodebaseContext context) {
|
|
||||||
return resolveTypeToFqn(type, cu, context, Map.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
static String resolveTypeToFqn(
|
|
||||||
Type type,
|
|
||||||
CompilationUnit cu,
|
|
||||||
CodebaseContext context,
|
|
||||||
Map<String, String> typeVarBindings) {
|
|
||||||
if (type == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (type instanceof WildcardType wildcardType) {
|
|
||||||
Type bound = wildcardType.getBound();
|
|
||||||
return bound != null ? resolveTypeToFqn(bound, cu, context, typeVarBindings) : null;
|
|
||||||
}
|
|
||||||
String simpleName;
|
|
||||||
if (type.isSimpleType()) {
|
|
||||||
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
|
|
||||||
} else if (type.isParameterizedType()) {
|
|
||||||
return resolveTypeToFqn(((ParameterizedType) type).getType(), cu, context, typeVarBindings);
|
|
||||||
} else {
|
|
||||||
simpleName = type.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (simpleName.contains("<")) {
|
|
||||||
simpleName = simpleName.substring(0, simpleName.indexOf('<'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeVarBindings.containsKey(simpleName)) {
|
|
||||||
return typeVarBindings.get(simpleName);
|
|
||||||
}
|
|
||||||
|
|
||||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
|
||||||
if (td != null) {
|
|
||||||
return context.getFqn(td);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Object impObj : cu.imports()) {
|
|
||||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
|
||||||
String impName = imp.getName().getFullyQualifiedName();
|
|
||||||
if (impName.endsWith("." + simpleName)) {
|
|
||||||
return impName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
|
||||||
if (!packageName.isEmpty()) {
|
|
||||||
AbstractTypeDeclaration localTd = context.getAbstractTypeDeclaration(packageName + "." + simpleName);
|
|
||||||
if (localTd != null) {
|
|
||||||
return context.getFqn(localTd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return simpleName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,450 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses Maven/Gradle build files statically to discover modules and local dependency wiring.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class StaticBuildFileAnalyzer {
|
|
||||||
|
|
||||||
private static final Pattern GRADLE_PROJECT_DEP = Pattern.compile("project\\(['\"]:(.*?)['\"]\\)");
|
|
||||||
private static final Pattern QUOTED_TOKEN = Pattern.compile("['\"]([^'\"]+)['\"]");
|
|
||||||
private static final Pattern GRADLE_PROJECT_DIR = Pattern.compile(
|
|
||||||
"project\\(['\"]:(.*?)['\"]\\)\\.projectDir\\s*=\\s*(?:file\\(['\"](.*?)['\"]\\)|new File\\(.*?,\\s*['\"](.*?)['\"]\\))");
|
|
||||||
private static final Pattern MAVEN_MODULE = Pattern.compile("<module>(.*?)</module>");
|
|
||||||
private static final Pattern MAVEN_DEPENDENCY_BLOCK = Pattern.compile("<dependency>(.*?)</dependency>", Pattern.DOTALL);
|
|
||||||
private static final Pattern MAVEN_TAG = Pattern.compile("<(groupId|artifactId)>(.*?)</\\1>");
|
|
||||||
|
|
||||||
public ProjectModuleGraph analyze(Path inputDir) throws IOException {
|
|
||||||
Path workspaceRoot = findWorkspaceRoot(inputDir);
|
|
||||||
Path indexRoot = findIndexingRoot(inputDir, workspaceRoot);
|
|
||||||
if (workspaceRoot == null) {
|
|
||||||
workspaceRoot = inputDir.toAbsolutePath().normalize();
|
|
||||||
}
|
|
||||||
if (indexRoot == null) {
|
|
||||||
indexRoot = workspaceRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, ProjectModuleGraph.ProjectModule> modulesById = new LinkedHashMap<>();
|
|
||||||
Map<ProjectModuleGraph.MavenCoordinates, ProjectModuleGraph.ProjectModule> modulesByCoordinates = new LinkedHashMap<>();
|
|
||||||
List<ProjectModuleGraph.ModuleDependency> dependencies = new ArrayList<>();
|
|
||||||
|
|
||||||
Path gradleSettingsRoot = Files.exists(workspaceRoot.resolve("settings.gradle"))
|
|
||||||
|| Files.exists(workspaceRoot.resolve("settings.gradle.kts"))
|
|
||||||
? workspaceRoot
|
|
||||||
: indexRoot;
|
|
||||||
Map<String, Path> gradleModulePaths = parseGradleIncludes(gradleSettingsRoot);
|
|
||||||
for (Map.Entry<String, Path> entry : gradleModulePaths.entrySet()) {
|
|
||||||
registerGradleModule(entry.getKey(), entry.getValue(), modulesById);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Path> pomFiles = findPomFiles(indexRoot);
|
|
||||||
for (Path pom : pomFiles) {
|
|
||||||
registerMavenSubmodules(pom, modulesById, modulesByCoordinates);
|
|
||||||
}
|
|
||||||
for (Path pom : pomFiles) {
|
|
||||||
registerMavenModule(pom, modulesById, modulesByCoordinates);
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureInputModule(inputDir, modulesById);
|
|
||||||
|
|
||||||
for (ProjectModuleGraph.ProjectModule module : List.copyOf(modulesById.values())) {
|
|
||||||
if (module.buildSystem() == ProjectModuleGraph.BuildSystem.GRADLE
|
|
||||||
|| module.buildSystem() == ProjectModuleGraph.BuildSystem.MIXED) {
|
|
||||||
dependencies.addAll(parseGradleDependencies(module, modulesById, gradleModulePaths));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (Path pom : pomFiles) {
|
|
||||||
dependencies.addAll(parseMavenDependencies(pom, modulesById, modulesByCoordinates));
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Static project graph: {} modules, {} dependencies under {}", modulesById.size(), dependencies.size(), workspaceRoot);
|
|
||||||
return new ProjectModuleGraph(workspaceRoot, modulesById, modulesByCoordinates, dependencies);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Path> parseGradleIncludes(Path gradleRoot) throws IOException {
|
|
||||||
Map<String, Path> includes = new LinkedHashMap<>();
|
|
||||||
if (gradleRoot == null) {
|
|
||||||
return includes;
|
|
||||||
}
|
|
||||||
Path settingsFile = gradleRoot.resolve("settings.gradle");
|
|
||||||
if (!Files.exists(settingsFile)) {
|
|
||||||
settingsFile = gradleRoot.resolve("settings.gradle.kts");
|
|
||||||
}
|
|
||||||
if (!Files.exists(settingsFile)) {
|
|
||||||
return includes;
|
|
||||||
}
|
|
||||||
|
|
||||||
String content = Files.readString(settingsFile);
|
|
||||||
for (String includePath : extractGradleIncludePaths(content)) {
|
|
||||||
String normalized = normalizeGradlePath(includePath);
|
|
||||||
Path moduleDir = resolveGradleModuleDirectory(gradleRoot, normalized, content);
|
|
||||||
if (Files.isDirectory(moduleDir)) {
|
|
||||||
includes.put(normalized, moduleDir.normalize());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return includes;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Set<String> extractGradleIncludePaths(String settingsContent) {
|
|
||||||
Set<String> paths = new LinkedHashSet<>();
|
|
||||||
for (String line : settingsContent.split("\\R")) {
|
|
||||||
if (!line.contains("include")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int includeIndex = line.indexOf("include");
|
|
||||||
Matcher tokenMatcher = QUOTED_TOKEN.matcher(line);
|
|
||||||
while (tokenMatcher.find()) {
|
|
||||||
if (tokenMatcher.start() > includeIndex) {
|
|
||||||
paths.add(normalizeGradlePath(tokenMatcher.group(1)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return paths;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String normalizeGradlePath(String includePath) {
|
|
||||||
String path = includePath.trim();
|
|
||||||
if (path.startsWith(":")) {
|
|
||||||
path = path.substring(1);
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Path resolveGradleModuleDirectory(Path gradleRoot, String gradlePath, String settingsContent) {
|
|
||||||
String lookup = gradlePath.contains(":") ? gradlePath : gradlePath.replace('/', ':');
|
|
||||||
Matcher matcher = GRADLE_PROJECT_DIR.matcher(settingsContent);
|
|
||||||
while (matcher.find()) {
|
|
||||||
String configuredPath = matcher.group(1);
|
|
||||||
if (configuredPath != null && configuredPath.equals(lookup)) {
|
|
||||||
String custom = matcher.group(2) != null ? matcher.group(2) : matcher.group(3);
|
|
||||||
if (custom != null) {
|
|
||||||
return gradleRoot.resolve(custom).normalize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return gradleRoot.resolve(gradlePath.replace(':', '/')).normalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void registerGradleModule(String gradlePath, Path directory, Map<String, ProjectModuleGraph.ProjectModule> modulesById) {
|
|
||||||
String id = "gradle:" + gradlePath;
|
|
||||||
ProjectModuleGraph.ProjectModule existing = modulesById.get(id);
|
|
||||||
if (existing != null) {
|
|
||||||
modulesById.put(id, new ProjectModuleGraph.ProjectModule(
|
|
||||||
id,
|
|
||||||
directory,
|
|
||||||
ProjectModuleGraph.BuildSystem.MIXED,
|
|
||||||
existing.mavenCoordinates()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
modulesById.put(id, new ProjectModuleGraph.ProjectModule(
|
|
||||||
id,
|
|
||||||
directory,
|
|
||||||
ProjectModuleGraph.BuildSystem.GRADLE,
|
|
||||||
null));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void registerMavenSubmodules(
|
|
||||||
Path pomFile,
|
|
||||||
Map<String, ProjectModuleGraph.ProjectModule> modulesById,
|
|
||||||
Map<ProjectModuleGraph.MavenCoordinates, ProjectModuleGraph.ProjectModule> modulesByCoordinates) throws IOException {
|
|
||||||
String content = Files.readString(pomFile);
|
|
||||||
Path parentDir = pomFile.getParent();
|
|
||||||
Matcher moduleMatcher = MAVEN_MODULE.matcher(content);
|
|
||||||
while (moduleMatcher.find()) {
|
|
||||||
Path moduleDir = parentDir.resolve(moduleMatcher.group(1).trim()).normalize();
|
|
||||||
Path childPom = moduleDir.resolve("pom.xml");
|
|
||||||
if (Files.exists(childPom)) {
|
|
||||||
registerMavenModule(childPom, modulesById, modulesByCoordinates);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void registerMavenModule(
|
|
||||||
Path pomFile,
|
|
||||||
Map<String, ProjectModuleGraph.ProjectModule> modulesById,
|
|
||||||
Map<ProjectModuleGraph.MavenCoordinates, ProjectModuleGraph.ProjectModule> modulesByCoordinates) throws IOException {
|
|
||||||
String content = Files.readString(pomFile);
|
|
||||||
Optional<ProjectModuleGraph.MavenCoordinates> coordinates = extractOwnMavenCoordinates(content);
|
|
||||||
if (coordinates.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Path directory = pomFile.getParent().normalize();
|
|
||||||
|
|
||||||
for (Map.Entry<String, ProjectModuleGraph.ProjectModule> entry : modulesById.entrySet()) {
|
|
||||||
if (entry.getValue().directory().toAbsolutePath().normalize().equals(directory.toAbsolutePath().normalize())) {
|
|
||||||
ProjectModuleGraph.ProjectModule merged = new ProjectModuleGraph.ProjectModule(
|
|
||||||
entry.getKey(),
|
|
||||||
directory,
|
|
||||||
ProjectModuleGraph.BuildSystem.MIXED,
|
|
||||||
coordinates.get());
|
|
||||||
modulesById.put(entry.getKey(), merged);
|
|
||||||
modulesByCoordinates.put(coordinates.get(), merged);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String id = "maven:" + coordinates.get().key();
|
|
||||||
ProjectModuleGraph.ProjectModule module = new ProjectModuleGraph.ProjectModule(
|
|
||||||
id,
|
|
||||||
directory,
|
|
||||||
ProjectModuleGraph.BuildSystem.MAVEN,
|
|
||||||
coordinates.get());
|
|
||||||
modulesById.put(id, module);
|
|
||||||
modulesByCoordinates.put(coordinates.get(), module);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Optional<ProjectModuleGraph.MavenCoordinates> extractOwnMavenCoordinates(String pomContent) {
|
|
||||||
String content = pomContent;
|
|
||||||
int parentEnd = pomContent.indexOf("</parent>");
|
|
||||||
if (parentEnd != -1) {
|
|
||||||
content = pomContent.substring(parentEnd);
|
|
||||||
}
|
|
||||||
String groupId = extractFirstTag(content, "groupId").orElse(null);
|
|
||||||
String artifactId = extractFirstTag(content, "artifactId").orElse(null);
|
|
||||||
if (artifactId == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
if (groupId == null) {
|
|
||||||
groupId = extractParentTag(pomContent, "groupId").orElse("unknown");
|
|
||||||
}
|
|
||||||
return Optional.of(new ProjectModuleGraph.MavenCoordinates(groupId, artifactId));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Optional<String> extractParentTag(String pomContent, String tag) {
|
|
||||||
Matcher parentMatcher = Pattern.compile("<parent>.*?</parent>", Pattern.DOTALL).matcher(pomContent);
|
|
||||||
if (!parentMatcher.find()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return extractFirstTag(parentMatcher.group(), tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Optional<String> extractFirstTag(String content, String tag) {
|
|
||||||
Matcher matcher = Pattern.compile("<" + tag + ">(.*?)</" + tag + ">").matcher(content);
|
|
||||||
if (matcher.find()) {
|
|
||||||
return Optional.of(matcher.group(1).trim());
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ProjectModuleGraph.ModuleDependency> parseGradleDependencies(
|
|
||||||
ProjectModuleGraph.ProjectModule module,
|
|
||||||
Map<String, ProjectModuleGraph.ProjectModule> modulesById,
|
|
||||||
Map<String, Path> gradleModulePaths) throws IOException {
|
|
||||||
List<ProjectModuleGraph.ModuleDependency> result = new ArrayList<>();
|
|
||||||
Path buildFile = module.directory().resolve("build.gradle");
|
|
||||||
if (!Files.exists(buildFile)) {
|
|
||||||
buildFile = module.directory().resolve("build.gradle.kts");
|
|
||||||
}
|
|
||||||
if (!Files.exists(buildFile)) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
String content = Files.readString(buildFile);
|
|
||||||
for (String line : content.split("\\R")) {
|
|
||||||
if (isTestGradleConfigurationLine(line)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Matcher matcher = GRADLE_PROJECT_DEP.matcher(line);
|
|
||||||
while (matcher.find()) {
|
|
||||||
String gradlePath = normalizeGradlePath(matcher.group(1));
|
|
||||||
String targetId = findGradleModuleId(gradlePath, modulesById, gradleModulePaths);
|
|
||||||
if (targetId != null) {
|
|
||||||
result.add(new ProjectModuleGraph.ModuleDependency(
|
|
||||||
module.id(), targetId, null, ProjectModuleGraph.DependencyKind.LOCAL_PROJECT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isTestScopedMavenDependency(String dependencyBlock) {
|
|
||||||
return extractFirstTag(dependencyBlock, "scope")
|
|
||||||
.map(scope -> scope.equalsIgnoreCase("test") || scope.equalsIgnoreCase("testCompile"))
|
|
||||||
.orElse(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isTestGradleConfigurationLine(String line) {
|
|
||||||
if (line == null || line.isBlank()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String trimmed = line.trim();
|
|
||||||
return trimmed.matches("(?i).*(testImplementation|testCompileOnly|testRuntimeOnly|testFixturesImplementation|androidTestImplementation|androidTestCompileOnly|androidTestRuntimeOnly|testAnnotationProcessor)\\b.*");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String findGradleModuleId(
|
|
||||||
String gradlePath,
|
|
||||||
Map<String, ProjectModuleGraph.ProjectModule> modulesById,
|
|
||||||
Map<String, Path> gradleModulePaths) {
|
|
||||||
String exact = "gradle:" + gradlePath;
|
|
||||||
if (modulesById.containsKey(exact)) {
|
|
||||||
return exact;
|
|
||||||
}
|
|
||||||
Path expectedDir = gradleModulePaths.get(gradlePath);
|
|
||||||
if (expectedDir != null) {
|
|
||||||
for (ProjectModuleGraph.ProjectModule module : modulesById.values()) {
|
|
||||||
if (module.directory().toAbsolutePath().normalize().equals(expectedDir.toAbsolutePath().normalize())) {
|
|
||||||
return module.id();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ProjectModuleGraph.ModuleDependency> parseMavenDependencies(
|
|
||||||
Path pomFile,
|
|
||||||
Map<String, ProjectModuleGraph.ProjectModule> modulesById,
|
|
||||||
Map<ProjectModuleGraph.MavenCoordinates, ProjectModuleGraph.ProjectModule> modulesByCoordinates) throws IOException {
|
|
||||||
List<ProjectModuleGraph.ModuleDependency> result = new ArrayList<>();
|
|
||||||
String content = Files.readString(pomFile);
|
|
||||||
Optional<ProjectModuleGraph.MavenCoordinates> fromCoordinates = extractOwnMavenCoordinates(content);
|
|
||||||
if (fromCoordinates.isEmpty()) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
ProjectModuleGraph.ProjectModule fromModule = modulesByCoordinates.get(fromCoordinates.get());
|
|
||||||
if (fromModule == null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
int depsStart = content.indexOf("<dependencies>");
|
|
||||||
int depsEnd = content.indexOf("</dependencies>");
|
|
||||||
if (depsStart == -1 || depsEnd <= depsStart) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
String depsSection = content.substring(depsStart, depsEnd);
|
|
||||||
Matcher blockMatcher = MAVEN_DEPENDENCY_BLOCK.matcher(depsSection);
|
|
||||||
while (blockMatcher.find()) {
|
|
||||||
String block = blockMatcher.group(1);
|
|
||||||
if (isTestScopedMavenDependency(block)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String groupId = extractFirstTag(block, "groupId").orElse(null);
|
|
||||||
String artifactId = extractFirstTag(block, "artifactId").orElse(null);
|
|
||||||
if (artifactId == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (groupId == null) {
|
|
||||||
groupId = extractParentTag(content, "groupId").orElse("unknown");
|
|
||||||
}
|
|
||||||
ProjectModuleGraph.MavenCoordinates targetCoordinates = new ProjectModuleGraph.MavenCoordinates(groupId, artifactId);
|
|
||||||
ProjectModuleGraph.ProjectModule targetModule = modulesByCoordinates.get(targetCoordinates);
|
|
||||||
if (targetModule != null && !targetModule.id().equals(fromModule.id())) {
|
|
||||||
result.add(new ProjectModuleGraph.ModuleDependency(
|
|
||||||
fromModule.id(),
|
|
||||||
targetModule.id(),
|
|
||||||
null,
|
|
||||||
ProjectModuleGraph.DependencyKind.LOCAL_PROJECT));
|
|
||||||
} else {
|
|
||||||
result.add(new ProjectModuleGraph.ModuleDependency(
|
|
||||||
fromModule.id(),
|
|
||||||
null,
|
|
||||||
targetCoordinates,
|
|
||||||
ProjectModuleGraph.DependencyKind.EXTERNAL_LIBRARY));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ensureInputModule(Path inputDir, Map<String, ProjectModuleGraph.ProjectModule> modulesById) {
|
|
||||||
Path normalized = inputDir.toAbsolutePath().normalize();
|
|
||||||
boolean known = modulesById.values().stream()
|
|
||||||
.anyMatch(module -> module.directory().toAbsolutePath().normalize().equals(normalized));
|
|
||||||
if (!known) {
|
|
||||||
modulesById.put("input:" + normalized.getFileName(), new ProjectModuleGraph.ProjectModule(
|
|
||||||
"input:" + normalized.getFileName(),
|
|
||||||
normalized,
|
|
||||||
ProjectModuleGraph.BuildSystem.MIXED,
|
|
||||||
null));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Path> findPomFiles(Path workspaceRoot) throws IOException {
|
|
||||||
if (workspaceRoot == null || !Files.isDirectory(workspaceRoot)) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
try (Stream<Path> stream = Files.walk(workspaceRoot)) {
|
|
||||||
return stream.filter(path -> path.getFileName().toString().equals("pom.xml"))
|
|
||||||
.filter(path -> !path.toString().contains("/target/"))
|
|
||||||
.filter(path -> !path.toString().contains("\\target\\"))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Path findWorkspaceRoot(Path start) {
|
|
||||||
if (start == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Path normalized = start.toAbsolutePath().normalize();
|
|
||||||
Path current = normalized;
|
|
||||||
Path deepestMavenAggregator = null;
|
|
||||||
Path deepestGradleSettings = null;
|
|
||||||
Path deepestPom = null;
|
|
||||||
|
|
||||||
while (current != null) {
|
|
||||||
if (Files.exists(current.resolve("settings.gradle")) || Files.exists(current.resolve("settings.gradle.kts"))) {
|
|
||||||
deepestGradleSettings = current;
|
|
||||||
}
|
|
||||||
Path pom = current.resolve("pom.xml");
|
|
||||||
if (Files.exists(pom)) {
|
|
||||||
deepestPom = current;
|
|
||||||
try {
|
|
||||||
if (Files.readString(pom).contains("<modules>")) {
|
|
||||||
deepestMavenAggregator = current;
|
|
||||||
}
|
|
||||||
} catch (IOException ignored) {
|
|
||||||
// continue walking
|
|
||||||
}
|
|
||||||
}
|
|
||||||
current = current.getParent();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deepestMavenAggregator != null) {
|
|
||||||
return deepestMavenAggregator;
|
|
||||||
}
|
|
||||||
if (deepestGradleSettings != null) {
|
|
||||||
return deepestGradleSettings;
|
|
||||||
}
|
|
||||||
if (deepestPom != null) {
|
|
||||||
return deepestPom;
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
Path findIndexingRoot(Path start, Path workspaceRoot) {
|
|
||||||
if (workspaceRoot == null) {
|
|
||||||
return findIndexingRoot(start);
|
|
||||||
}
|
|
||||||
return workspaceRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
Path findIndexingRoot(Path start) {
|
|
||||||
if (start == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Path current = start.toAbsolutePath().normalize();
|
|
||||||
Path gitRoot = null;
|
|
||||||
while (current != null) {
|
|
||||||
if (Files.exists(current.resolve(".git"))) {
|
|
||||||
gitRoot = current;
|
|
||||||
}
|
|
||||||
current = current.getParent();
|
|
||||||
}
|
|
||||||
return gitRoot != null ? gitRoot : findWorkspaceRoot(start);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.AccessorResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.FieldInitializerFinder;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.MethodInvocationUnwrapper;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
@@ -26,10 +21,6 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
protected final VariableTracer variableTracer;
|
protected final VariableTracer variableTracer;
|
||||||
protected final ConstantExtractor constantExtractor;
|
protected final ConstantExtractor constantExtractor;
|
||||||
protected final ConstructorAnalyzer constructorAnalyzer;
|
protected final ConstructorAnalyzer constructorAnalyzer;
|
||||||
protected final ExpressionAccessClassifier expressionAccessClassifier;
|
|
||||||
protected final AccessorResolver accessorResolver;
|
|
||||||
protected final FieldInitializerFinder fieldInitializerFinder;
|
|
||||||
protected final PathBindingEvaluator pathBindingEvaluator;
|
|
||||||
private final Map<String, ASTNode> parsedNodeCache = new HashMap<>();
|
private final Map<String, ASTNode> parsedNodeCache = new HashMap<>();
|
||||||
|
|
||||||
private ASTNode parseExpressionString(String expr) {
|
private ASTNode parseExpressionString(String expr) {
|
||||||
@@ -72,7 +63,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
|
|
||||||
public AbstractCallGraphEngine(CodebaseContext context) {
|
public AbstractCallGraphEngine(CodebaseContext context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.constantResolver = context.getConstantResolver();
|
this.constantResolver = new ConstantResolver();
|
||||||
|
|
||||||
this.typeResolver = new TypeResolver(context);
|
this.typeResolver = new TypeResolver(context);
|
||||||
this.pathFinder = new CallGraphPathFinder(context);
|
this.pathFinder = new CallGraphPathFinder(context);
|
||||||
@@ -86,13 +77,6 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
this.constantExtractor.setConstructorAnalyzer(this.constructorAnalyzer);
|
this.constantExtractor.setConstructorAnalyzer(this.constructorAnalyzer);
|
||||||
this.constructorAnalyzer.setVariableTracer(this.variableTracer);
|
this.constructorAnalyzer.setVariableTracer(this.variableTracer);
|
||||||
this.constructorAnalyzer.setConstantExtractor(this.constantExtractor);
|
this.constructorAnalyzer.setConstantExtractor(this.constantExtractor);
|
||||||
this.expressionAccessClassifier = new ExpressionAccessClassifier(
|
|
||||||
context, variableTracer, constantResolver, this::parseExpressionString);
|
|
||||||
this.constantExtractor.setExpressionAccessClassifier(this.expressionAccessClassifier);
|
|
||||||
this.accessorResolver = new AccessorResolver(context, constantExtractor, constructorAnalyzer, constantResolver);
|
|
||||||
this.constantExtractor.setAccessorResolver(this.accessorResolver);
|
|
||||||
this.fieldInitializerFinder = new FieldInitializerFinder(context);
|
|
||||||
this.pathBindingEvaluator = new PathBindingEvaluator(context, variableTracer, constantResolver, typeResolver);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||||
@@ -104,48 +88,30 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
List<String> startMethods = new ArrayList<>();
|
List<String> startMethods = new ArrayList<>();
|
||||||
startMethods.add(startMethod);
|
startMethods.add(startMethod);
|
||||||
|
|
||||||
List<Map<String, String>> bindingVariants =
|
boolean foundAny = false;
|
||||||
EntryPointBindingExpander.expandPathVariableBindings(ep, context, callGraph);
|
for (TriggerPoint tp : triggers) {
|
||||||
if (bindingVariants.isEmpty()) {
|
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
|
||||||
bindingVariants = List.of(Map.of());
|
List<List<String>> allPaths = new ArrayList<>();
|
||||||
|
for (String sMethod : startMethods) {
|
||||||
|
allPaths.addAll(pathFinder.findAllPaths(sMethod, targetMethod, callGraph, new HashSet<>()));
|
||||||
|
}
|
||||||
|
Set<List<String>> uniquePaths = new LinkedHashSet<>(allPaths);
|
||||||
|
for (List<String> path : uniquePaths) {
|
||||||
|
foundAny = true;
|
||||||
|
TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph);
|
||||||
|
if (resolvedTp != null) {
|
||||||
|
String contextMachineId = pathFinder.extractContextMachineId(path, callGraph);
|
||||||
|
chains.add(CallChain.builder()
|
||||||
|
.entryPoint(ep)
|
||||||
|
.triggerPoint(resolvedTp)
|
||||||
|
.methodChain(path)
|
||||||
|
.contextMachineId(contextMachineId)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (!foundAny && log.isDebugEnabled()) {
|
||||||
for (Map<String, String> initialBindings : bindingVariants) {
|
log.debug("Entry point {} ({}) did not reach any trigger points. Graph nodes available: {}", ep.getName(), startMethod, callGraph.keySet());
|
||||||
EntryPoint chainEntryPoint = initialBindings.isEmpty()
|
|
||||||
? ep
|
|
||||||
: EntryPointBindingExpander.withResolvedPath(ep, initialBindings);
|
|
||||||
|
|
||||||
boolean foundAny = false;
|
|
||||||
for (TriggerPoint tp : triggers) {
|
|
||||||
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
|
|
||||||
List<List<String>> allPaths = new ArrayList<>();
|
|
||||||
for (String sMethod : startMethods) {
|
|
||||||
allPaths.addAll(pathFinder.findAllPaths(
|
|
||||||
sMethod, targetMethod, callGraph, new HashSet<>(),
|
|
||||||
pathBindingEvaluator, initialBindings));
|
|
||||||
}
|
|
||||||
Set<List<String>> uniquePaths = new LinkedHashSet<>(allPaths);
|
|
||||||
for (List<String> path : uniquePaths) {
|
|
||||||
if (!pathBindingEvaluator.isPathCompatible(path, callGraph, pathFinder, initialBindings)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
foundAny = true;
|
|
||||||
TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph);
|
|
||||||
if (resolvedTp != null) {
|
|
||||||
String contextMachineId = pathFinder.extractContextMachineId(path, callGraph);
|
|
||||||
chains.add(CallChain.builder()
|
|
||||||
.entryPoint(chainEntryPoint)
|
|
||||||
.triggerPoint(resolvedTp)
|
|
||||||
.methodChain(path)
|
|
||||||
.contextMachineId(contextMachineId)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!foundAny && log.isDebugEnabled()) {
|
|
||||||
log.debug("Entry point {} ({}) did not reach any trigger points. Graph nodes available: {}",
|
|
||||||
chainEntryPoint.getName(), startMethod, callGraph.keySet());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return chains;
|
return chains;
|
||||||
@@ -190,8 +156,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
String currentParamName = event;
|
String currentParamName = event;
|
||||||
String resolvedValue = event;
|
String resolvedValue = event;
|
||||||
String methodSuffix = "";
|
String methodSuffix = "";
|
||||||
String scopeMethod = path.isEmpty() ? null : path.get(0);
|
String[] extractedEntry = extractMethodSuffix(currentParamName, methodSuffix);
|
||||||
String[] extractedEntry = extractMethodSuffix(currentParamName, methodSuffix, scopeMethod);
|
|
||||||
currentParamName = extractedEntry[0];
|
currentParamName = extractedEntry[0];
|
||||||
methodSuffix = extractedEntry[1];
|
methodSuffix = extractedEntry[1];
|
||||||
boolean isAmbiguous = false;
|
boolean isAmbiguous = false;
|
||||||
@@ -232,7 +197,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
Map<String, String> parameterValues = variableTracer.buildParameterValuesMap(caller, target, callGraph, path, i);
|
Map<String, String> parameterValues = variableTracer.buildParameterValuesMap(caller, target, callGraph, path, i);
|
||||||
String tracedVar = variableTracer.traceLocalVariable(target, currentParamName, parameterValues);
|
String tracedVar = variableTracer.traceLocalVariable(target, currentParamName, parameterValues);
|
||||||
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
|
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
|
||||||
String[] extractedTraced = extractMethodSuffix(tracedVar, methodSuffix, target);
|
String[] extractedTraced = extractMethodSuffix(tracedVar, methodSuffix);
|
||||||
tracedVar = extractedTraced[0];
|
tracedVar = extractedTraced[0];
|
||||||
methodSuffix = extractedTraced[1];
|
methodSuffix = extractedTraced[1];
|
||||||
currentParamName = tracedVar;
|
currentParamName = tracedVar;
|
||||||
@@ -294,7 +259,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
// Intentionally not replacing 'super' because super should remain statically bound to the superclass of where the code is defined.
|
// Intentionally not replacing 'super' because super should remain statically bound to the superclass of where the code is defined.
|
||||||
}
|
}
|
||||||
String[] extractedArg = extractMethodSuffix(arg, methodSuffix, caller);
|
String[] extractedArg = extractMethodSuffix(arg, methodSuffix);
|
||||||
arg = extractedArg[0];
|
arg = extractedArg[0];
|
||||||
methodSuffix = extractedArg[1];
|
methodSuffix = extractedArg[1];
|
||||||
currentParamName = arg;
|
currentParamName = arg;
|
||||||
@@ -325,43 +290,46 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String entryMethod = path.get(0);
|
String entryMethod = path.get(0);
|
||||||
|
|
||||||
if (methodSuffix != null && expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)) {
|
|
||||||
List<String> chainedGetterEvents = evaluateGetterOnReceiver(resolvedValue, methodSuffix, entryMethod, path, callGraph);
|
|
||||||
if (chainedGetterEvents != null && !chainedGetterEvents.isEmpty()) {
|
|
||||||
log.debug("Early return (chained getter): getterEvents = {}", chainedGetterEvents);
|
|
||||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, chainedGetterEvents);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)) {
|
|
||||||
List<String> setterEvents = resolveSetterEventsFromSuffix(currentParamName, methodSuffix, entryMethod, path);
|
|
||||||
if (!setterEvents.isEmpty()) {
|
|
||||||
log.debug("Early return (scoped setter): setterEvents = {}", setterEvents);
|
|
||||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, setterEvents);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int entryParamIndex = typeResolver.getParameterIndex(entryMethod, currentParamName);
|
int entryParamIndex = typeResolver.getParameterIndex(entryMethod, currentParamName);
|
||||||
if (entryParamIndex < 0) {
|
if (entryParamIndex < 0) {
|
||||||
if (methodSuffix != null && expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)) {
|
if (methodSuffix.startsWith(".get") || methodSuffix.equals(".type") || methodSuffix.equals(".event") || methodSuffix.equals(".type()") || methodSuffix.equals(".event()")) {
|
||||||
List<String> setterEvents = resolveSetterEventsFromSuffix(currentParamName, methodSuffix, entryMethod, path);
|
String localMethodName = methodSuffix.substring(1).replace("()", "");
|
||||||
if (!setterEvents.isEmpty()) {
|
Expression localSetterExpr = variableTracer.traceLocalSetter(entryMethod, currentParamName, localMethodName);
|
||||||
log.debug("Early return 1: setterEvents = {}", setterEvents);
|
if (localSetterExpr != null) {
|
||||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, setterEvents);
|
List<String> setterEvents = new ArrayList<>();
|
||||||
|
List<Expression> tracedSetters = variableTracer.traceVariableAll(localSetterExpr);
|
||||||
|
for (Expression tracedSetter : tracedSetters) {
|
||||||
|
constantExtractor.extractConstantsFromExpression(tracedSetter, setterEvents);
|
||||||
|
}
|
||||||
|
if (!setterEvents.isEmpty()) {
|
||||||
|
log.debug("Early return 1: setterEvents = {}", setterEvents);
|
||||||
|
return TriggerPoint.builder()
|
||||||
|
.event(tp.getEvent())
|
||||||
|
.className(tp.getClassName())
|
||||||
|
.methodName(tp.getMethodName())
|
||||||
|
.sourceFile(tp.getSourceFile())
|
||||||
|
.sourceModule(tp.getSourceModule())
|
||||||
|
.stateMachineId(tp.getStateMachineId())
|
||||||
|
.sourceState(tp.getSourceState())
|
||||||
|
.lineNumber(tp.getLineNumber())
|
||||||
|
.polymorphicEvents(setterEvents)
|
||||||
|
.constraint(tp.getConstraint())
|
||||||
|
.stateTypeFqn(tp.getStateTypeFqn())
|
||||||
|
.eventTypeFqn(tp.getEventTypeFqn())
|
||||||
|
.external(tp.isExternal())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hasGetterOnReceiver = methodSuffix != null
|
boolean hasGetterOnReceiver = (methodSuffix.startsWith(".get") || methodSuffix.equals(".type") || methodSuffix.equals(".event") || methodSuffix.equals(".type()") || methodSuffix.equals(".event()")) && currentParamName != null
|
||||||
&& expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)
|
|
||||||
&& currentParamName != null
|
|
||||||
&& (currentParamName.contains("new ") || (!currentParamName.replaceFirst("^this\\.", "").contains(".") && !currentParamName.contains("(")));
|
&& (currentParamName.contains("new ") || (!currentParamName.replaceFirst("^this\\.", "").contains(".") && !currentParamName.contains("(")));
|
||||||
if (hasGetterOnReceiver) {
|
if (hasGetterOnReceiver) {
|
||||||
List<String> getterEvents = evaluateGetterOnReceiver(resolvedValue, methodSuffix, entryMethod, path, callGraph);
|
List<String> getterEvents = evaluateGetterOnReceiver(resolvedValue, methodSuffix, entryMethod, path, callGraph);
|
||||||
if (getterEvents != null && !getterEvents.isEmpty()) {
|
if (getterEvents != null && !getterEvents.isEmpty()) {
|
||||||
log.debug("Early return 2: getterEvents = {}", getterEvents);
|
log.debug("Early return 2: getterEvents = {}", getterEvents);
|
||||||
return TriggerPoint.builder()
|
return TriggerPoint.builder()
|
||||||
.event(resolvedValue)
|
.event(tp.getEvent())
|
||||||
.className(tp.getClassName())
|
.className(tp.getClassName())
|
||||||
.methodName(tp.getMethodName())
|
.methodName(tp.getMethodName())
|
||||||
.sourceFile(tp.getSourceFile())
|
.sourceFile(tp.getSourceFile())
|
||||||
@@ -380,7 +348,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
|
|
||||||
String tracedVar = variableTracer.traceLocalVariable(entryMethod, currentParamName);
|
String tracedVar = variableTracer.traceLocalVariable(entryMethod, currentParamName);
|
||||||
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
|
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
|
||||||
String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix, entryMethod);
|
String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix);
|
||||||
tracedVar = extractedFinalTraced[0];
|
tracedVar = extractedFinalTraced[0];
|
||||||
methodSuffix = extractedFinalTraced[1];
|
methodSuffix = extractedFinalTraced[1];
|
||||||
currentParamName = tracedVar;
|
currentParamName = tracedVar;
|
||||||
@@ -398,7 +366,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
if (!polymorphicEvents.contains(parsed)) polymorphicEvents.add(parsed);
|
if (!polymorphicEvents.contains(parsed)) polymorphicEvents.add(parsed);
|
||||||
}
|
}
|
||||||
return TriggerPoint.builder()
|
return TriggerPoint.builder()
|
||||||
.event(resolvedValue)
|
.event(tp.getEvent())
|
||||||
.className(tp.getClassName())
|
.className(tp.getClassName())
|
||||||
.methodName(tp.getMethodName())
|
.methodName(tp.getMethodName())
|
||||||
.sourceFile(tp.getSourceFile())
|
.sourceFile(tp.getSourceFile())
|
||||||
@@ -426,62 +394,17 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
|
|
||||||
if (exprNode instanceof MethodInvocation mi) {
|
if (exprNode instanceof MethodInvocation mi) {
|
||||||
methodName = mi.getName().getIdentifier();
|
methodName = mi.getName().getIdentifier();
|
||||||
if ("get".equals(methodName) || "getOrDefault".equals(methodName)) {
|
|
||||||
boolean keyedLookup = expressionAccessClassifier.isKeyedLookup(mi, entryMethod);
|
|
||||||
boolean functionalGet = "get".equals(methodName) && mi.arguments().isEmpty() && !keyedLookup;
|
|
||||||
if (keyedLookup || functionalGet) {
|
|
||||||
constantExtractor.extractConstantsFromExpression((Expression) exprNode, polymorphicEvents);
|
|
||||||
if (!polymorphicEvents.isEmpty()) {
|
|
||||||
return TriggerPoint.builder()
|
|
||||||
.event(resolvedValue)
|
|
||||||
.className(tp.getClassName())
|
|
||||||
.methodName(tp.getMethodName())
|
|
||||||
.sourceFile(tp.getSourceFile())
|
|
||||||
.sourceModule(tp.getSourceModule())
|
|
||||||
.stateMachineId(tp.getStateMachineId())
|
|
||||||
.sourceState(tp.getSourceState())
|
|
||||||
.lineNumber(tp.getLineNumber())
|
|
||||||
.polymorphicEvents(polymorphicEvents)
|
|
||||||
.constraint(tp.getConstraint())
|
|
||||||
.stateTypeFqn(tp.getStateTypeFqn())
|
|
||||||
.eventTypeFqn(tp.getEventTypeFqn())
|
|
||||||
.external(tp.isExternal())
|
|
||||||
.ambiguous(isAmbiguous)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
boolean handledStaticEnum = false;
|
boolean handledStaticEnum = false;
|
||||||
if (methodName.equals("valueOf") && mi.arguments().size() == 1) {
|
if (methodName.equals("valueOf") && mi.arguments().size() == 1) {
|
||||||
Object arg = mi.arguments().get(0);
|
Object arg = mi.arguments().get(0);
|
||||||
String enumTypeName = resolveValueOfEnumTypeName(mi);
|
|
||||||
if (arg instanceof StringLiteral sl) {
|
if (arg instanceof StringLiteral sl) {
|
||||||
if (enumTypeName != null) {
|
polymorphicEvents.add(sl.getLiteralValue());
|
||||||
polymorphicEvents.add(enumTypeName + "." + sl.getLiteralValue());
|
|
||||||
} else {
|
|
||||||
polymorphicEvents.add(sl.getLiteralValue());
|
|
||||||
}
|
|
||||||
methodName = null;
|
methodName = null;
|
||||||
handledStaticEnum = true;
|
handledStaticEnum = true;
|
||||||
} else if (arg instanceof QualifiedName qn) {
|
} else if (arg instanceof QualifiedName qn) {
|
||||||
if (enumTypeName != null) {
|
polymorphicEvents.add(qn.getName().getIdentifier());
|
||||||
polymorphicEvents.add(enumTypeName + "." + qn.getName().getIdentifier());
|
|
||||||
} else {
|
|
||||||
polymorphicEvents.add(qn.getName().getIdentifier());
|
|
||||||
}
|
|
||||||
methodName = null;
|
methodName = null;
|
||||||
handledStaticEnum = true;
|
handledStaticEnum = true;
|
||||||
} else if (arg instanceof SimpleName sn) {
|
|
||||||
String resolvedConstant = resolveEnumConstantFromArgument(sn.getIdentifier(), path, callGraph);
|
|
||||||
if (resolvedConstant != null) {
|
|
||||||
if (enumTypeName != null) {
|
|
||||||
polymorphicEvents.add(enumTypeName + "." + resolvedConstant);
|
|
||||||
} else {
|
|
||||||
polymorphicEvents.add(resolvedConstant);
|
|
||||||
}
|
|
||||||
methodName = null;
|
|
||||||
handledStaticEnum = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (methodName.equals("name") && mi.arguments().isEmpty()) {
|
} else if (methodName.equals("name") && mi.arguments().isEmpty()) {
|
||||||
if (mi.getExpression() instanceof QualifiedName qn) {
|
if (mi.getExpression() instanceof QualifiedName qn) {
|
||||||
@@ -521,7 +444,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return TriggerPoint.builder()
|
return TriggerPoint.builder()
|
||||||
.event(resolvedValue)
|
.event(tp.getEvent())
|
||||||
.className(tp.getClassName())
|
.className(tp.getClassName())
|
||||||
.methodName(tp.getMethodName())
|
.methodName(tp.getMethodName())
|
||||||
.sourceFile(tp.getSourceFile())
|
.sourceFile(tp.getSourceFile())
|
||||||
@@ -656,8 +579,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
|
|
||||||
if (methodName != null) {
|
if (methodName != null) {
|
||||||
if (varName != null && !varName.isEmpty() && Character.isLowerCase(varName.charAt(0)) && !methodName.equals("VariableReference")) {
|
if (varName != null && !varName.isEmpty() && Character.isLowerCase(varName.charAt(0)) && !methodName.equals("VariableReference")) {
|
||||||
String scopeForVar = resolveScopeMethodForVariable(varName, entryMethod, path);
|
Expression localSetterExpr = variableTracer.traceLocalSetter(entryMethod, varName, methodName);
|
||||||
Expression localSetterExpr = variableTracer.traceLocalSetter(scopeForVar, varName, methodName);
|
|
||||||
if (localSetterExpr != null) {
|
if (localSetterExpr != null) {
|
||||||
List<Expression> tracedSetters = variableTracer.traceVariableAll(localSetterExpr);
|
List<Expression> tracedSetters = variableTracer.traceVariableAll(localSetterExpr);
|
||||||
for (Expression tracedSetter : tracedSetters) {
|
for (Expression tracedSetter : tracedSetters) {
|
||||||
@@ -802,14 +724,10 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
if (declaredType != null) {
|
if (declaredType != null) {
|
||||||
List<String> enumValues = context.getEnumValues(declaredType);
|
List<String> enumValues = context.getEnumValues(declaredType);
|
||||||
if (enumValues != null && !enumValues.isEmpty()) {
|
if (enumValues != null && !enumValues.isEmpty()) {
|
||||||
if (!hasConcreteEnumConstants(polymorphicEvents, declaredType, context)
|
for (String ev : enumValues) {
|
||||||
&& !tp.isExternal()
|
polymorphicEvents.add(constantExtractor.parseEnumSetElement(ev));
|
||||||
&& !isRuntimeEnumParameter(exprNode instanceof Expression expression ? expression : null)) {
|
|
||||||
for (String ev : enumValues) {
|
|
||||||
polymorphicEvents.add(constantExtractor.parseEnumSetElement(ev));
|
|
||||||
}
|
|
||||||
isAmbiguous = true;
|
|
||||||
}
|
}
|
||||||
|
isAmbiguous = true;
|
||||||
} else {
|
} else {
|
||||||
List<String> typesToInspect = new ArrayList<>();
|
List<String> typesToInspect = new ArrayList<>();
|
||||||
if ("inline-instantiation".equals(sourceMethod)) {
|
if ("inline-instantiation".equals(sourceMethod)) {
|
||||||
@@ -931,11 +849,6 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
polymorphicEvents = unpacked;
|
polymorphicEvents = unpacked;
|
||||||
|
|
||||||
polymorphicEvents = polymorphicEvents.stream().distinct().collect(java.util.stream.Collectors.toList());
|
polymorphicEvents = polymorphicEvents.stream().distinct().collect(java.util.stream.Collectors.toList());
|
||||||
|
|
||||||
Expression runtimeExpr = exprNode instanceof Expression expression ? expression : null;
|
|
||||||
if (isRuntimeEnumParameter(runtimeExpr) || tp.isExternal()) {
|
|
||||||
polymorphicEvents.removeIf(pe -> pe != null && pe.startsWith("<SYMBOLIC:"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) {
|
if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) {
|
||||||
return TriggerPoint.builder()
|
return TriggerPoint.builder()
|
||||||
@@ -1117,202 +1030,20 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return constructorAnalyzer.findReturnedCIC(className, methodName, context);
|
return constructorAnalyzer.findReturnedCIC(className, methodName, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected record GetterChainReceiver(String typeFqn, ClassInstanceCreation cic) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves a nested getter prefix such as {@code wrapper.getEvent()} to the declared return type
|
|
||||||
* and, when possible, a {@code new ...()} instance from the getter body.
|
|
||||||
*/
|
|
||||||
private TriggerPoint buildTriggerPointWithPolymorphicEvents(TriggerPoint tp, String resolvedValue, List<String> polymorphicEvents) {
|
|
||||||
return TriggerPoint.builder()
|
|
||||||
.event(resolvedValue)
|
|
||||||
.className(tp.getClassName())
|
|
||||||
.methodName(tp.getMethodName())
|
|
||||||
.sourceFile(tp.getSourceFile())
|
|
||||||
.sourceModule(tp.getSourceModule())
|
|
||||||
.stateMachineId(tp.getStateMachineId())
|
|
||||||
.sourceState(tp.getSourceState())
|
|
||||||
.lineNumber(tp.getLineNumber())
|
|
||||||
.polymorphicEvents(polymorphicEvents)
|
|
||||||
.constraint(tp.getConstraint())
|
|
||||||
.stateTypeFqn(tp.getStateTypeFqn())
|
|
||||||
.eventTypeFqn(tp.getEventTypeFqn())
|
|
||||||
.external(tp.isExternal())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> resolveSetterEventsFromSuffix(
|
|
||||||
String currentParamName,
|
|
||||||
String methodSuffix,
|
|
||||||
String entryMethod,
|
|
||||||
List<String> path) {
|
|
||||||
String localMethodName = methodSuffix.substring(1).replace("()", "");
|
|
||||||
String scopeForVar = resolveScopeMethodForVariable(currentParamName, entryMethod, path);
|
|
||||||
Expression localSetterExpr = variableTracer.traceLocalSetter(scopeForVar, currentParamName, localMethodName);
|
|
||||||
if (localSetterExpr == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
List<String> setterEvents = new ArrayList<>();
|
|
||||||
List<Expression> tracedSetters = variableTracer.traceVariableAll(localSetterExpr);
|
|
||||||
for (Expression tracedSetter : tracedSetters) {
|
|
||||||
constantExtractor.extractConstantsFromExpression(tracedSetter, setterEvents);
|
|
||||||
}
|
|
||||||
return setterEvents;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected GetterChainReceiver resolveGetterChainToReceiver(Expression expr, String entryMethod) {
|
|
||||||
if (expr instanceof SimpleName sn) {
|
|
||||||
String type = variableTracer.getVariableDeclaredType(entryMethod, sn.getIdentifier());
|
|
||||||
if (type == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Expression init = findVariableInitializer(entryMethod, sn.getIdentifier());
|
|
||||||
ClassInstanceCreation cic = init instanceof ClassInstanceCreation c ? c : null;
|
|
||||||
return new GetterChainReceiver(type, cic);
|
|
||||||
}
|
|
||||||
if (expr instanceof ThisExpression) {
|
|
||||||
int lastDot = entryMethod.lastIndexOf('.');
|
|
||||||
if (lastDot <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String className = entryMethod.substring(0, lastDot);
|
|
||||||
return new GetterChainReceiver(className, null);
|
|
||||||
}
|
|
||||||
if (expr instanceof ClassInstanceCreation cic) {
|
|
||||||
return new GetterChainReceiver(
|
|
||||||
click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()),
|
|
||||||
cic);
|
|
||||||
}
|
|
||||||
if (!(expr instanceof MethodInvocation mi)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
GetterChainReceiver base = resolveGetterChainToReceiver(mi.getExpression(), entryMethod);
|
|
||||||
if (base == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String getterName = mi.getName().getIdentifier();
|
|
||||||
String simpleType = simplifyTypeName(base.typeFqn());
|
|
||||||
String ownerFqn = resolveOwnerFqn(simpleType, base.typeFqn());
|
|
||||||
|
|
||||||
AccessorResolver.GetterChainStep indexedStep =
|
|
||||||
accessorResolver.resolveGetterChainStep(ownerFqn, getterName, ResolutionBudget.defaults());
|
|
||||||
if (indexedStep != null) {
|
|
||||||
return new GetterChainReceiver(indexedStep.typeFqn(), indexedStep.cic());
|
|
||||||
}
|
|
||||||
|
|
||||||
ClassInstanceCreation nextCic = constructorAnalyzer.findReturnedCIC(simpleType, getterName, context);
|
|
||||||
if (nextCic != null) {
|
|
||||||
return new GetterChainReceiver(
|
|
||||||
click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(nextCic.getType()),
|
|
||||||
nextCic);
|
|
||||||
}
|
|
||||||
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(simpleType);
|
|
||||||
if (td == null) {
|
|
||||||
td = context.getTypeDeclaration(base.typeFqn());
|
|
||||||
}
|
|
||||||
if (td != null) {
|
|
||||||
MethodDeclaration getter = context.findMethodDeclaration(td, getterName, true);
|
|
||||||
if (getter != null && getter.getReturnType2() != null) {
|
|
||||||
return new GetterChainReceiver(getter.getReturnType2().toString(), null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveOwnerFqn(String simpleType, String typeFqn) {
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(simpleType);
|
|
||||||
if (td == null && typeFqn != null) {
|
|
||||||
td = context.getTypeDeclaration(typeFqn);
|
|
||||||
}
|
|
||||||
return td != null ? context.getFqn(td) : typeFqn;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String simplifyTypeName(String typeFqn) {
|
|
||||||
if (typeFqn == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String simple = typeFqn.contains(".") ? typeFqn.substring(typeFqn.lastIndexOf('.') + 1) : typeFqn;
|
|
||||||
if (simple.contains("<")) {
|
|
||||||
simple = simple.substring(0, simple.indexOf('<'));
|
|
||||||
}
|
|
||||||
return simple;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveScopeMethodForVariable(String varName, String entryMethod, List<String> path) {
|
|
||||||
if (varName != null && path != null) {
|
|
||||||
for (String methodFqn : path) {
|
|
||||||
if (variableTracer.getVariableDeclaredType(methodFqn, varName) != null) {
|
|
||||||
return methodFqn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return entryMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveClassForSuperCall(String entryMethod, List<String> path) {
|
|
||||||
if (path != null) {
|
|
||||||
for (String methodFqn : path) {
|
|
||||||
if (methodFqn == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int lastDot = methodFqn.lastIndexOf('.');
|
|
||||||
if (lastDot <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String className = methodFqn.substring(0, lastDot);
|
|
||||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(className);
|
|
||||||
if (typeDeclaration == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String superFqn = context.getSuperclassFqn(typeDeclaration);
|
|
||||||
if (superFqn != null && !"java.lang.Object".equals(superFqn)) {
|
|
||||||
return className;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int lastDot = entryMethod.lastIndexOf('.');
|
|
||||||
return lastDot > 0 ? entryMethod.substring(0, lastDot) : entryMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected List<String> evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
protected List<String> evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||||
ASTNode exprNode = parseExpressionString(resolvedValue);
|
ASTNode exprNode = parseExpressionString(resolvedValue);
|
||||||
while (exprNode instanceof ParenthesizedExpression pe) {
|
while (exprNode instanceof ParenthesizedExpression pe) {
|
||||||
exprNode = pe.getExpression();
|
exprNode = pe.getExpression();
|
||||||
}
|
}
|
||||||
|
|
||||||
String scopeMethod = entryMethod;
|
|
||||||
Expression rootExpr = exprNode instanceof Expression e ? e : null;
|
|
||||||
if (rootExpr != null) {
|
|
||||||
while (rootExpr instanceof MethodInvocation mi) {
|
|
||||||
rootExpr = mi.getExpression();
|
|
||||||
}
|
|
||||||
if (rootExpr instanceof SimpleName sn) {
|
|
||||||
scopeMethod = resolveScopeMethodForVariable(sn.getIdentifier(), entryMethod, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exprNode instanceof SuperMethodInvocation smi) {
|
if (exprNode instanceof SuperMethodInvocation smi) {
|
||||||
String getterName = smi.getName().getIdentifier();
|
String getterName = smi.getName().getIdentifier();
|
||||||
String className = resolveClassForSuperCall(entryMethod, path);
|
int lastDot = entryMethod.lastIndexOf('.');
|
||||||
|
String className = lastDot > 0 ? entryMethod.substring(0, lastDot) : entryMethod;
|
||||||
TypeDeclaration currentTd = context.getTypeDeclaration(className);
|
TypeDeclaration currentTd = context.getTypeDeclaration(className);
|
||||||
if (currentTd != null) {
|
if (currentTd != null) {
|
||||||
String receiverType = context.getSuperclassFqn(currentTd);
|
String receiverType = context.getSuperclassFqn(currentTd);
|
||||||
if (receiverType != null) {
|
return constantExtractor.resolveMethodReturnConstant(receiverType, getterName, 0, new HashSet<>(), null);
|
||||||
CompilationUnit contextCu = currentTd.getRoot() instanceof CompilationUnit cu ? cu : null;
|
|
||||||
List<String> resolved = accessorResolver.resolve(
|
|
||||||
receiverType,
|
|
||||||
getterName,
|
|
||||||
new AccessorResolver.ReceiverContext(
|
|
||||||
context.getTypeDeclaration(receiverType), null, contextCu, false, null),
|
|
||||||
ResolutionBudget.defaults());
|
|
||||||
if (!resolved.isEmpty()) {
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
return constantExtractor.resolveMethodReturnConstant(receiverType, getterName, 0, new HashSet<>(), contextCu);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (exprNode instanceof MethodInvocation mi) {
|
} else if (exprNode instanceof MethodInvocation mi) {
|
||||||
String getterName = mi.getName().getIdentifier();
|
String getterName = mi.getName().getIdentifier();
|
||||||
@@ -1324,31 +1055,25 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
|
|
||||||
if (receiver instanceof SimpleName sn) {
|
if (receiver instanceof SimpleName sn) {
|
||||||
receiverName = sn.getIdentifier();
|
receiverName = sn.getIdentifier();
|
||||||
receiverType = variableTracer.getVariableDeclaredType(scopeMethod, receiverName);
|
receiverType = variableTracer.getVariableDeclaredType(entryMethod, receiverName);
|
||||||
} else if (receiver instanceof ClassInstanceCreation cic) {
|
} else if (receiver instanceof ClassInstanceCreation cic) {
|
||||||
directCic = cic;
|
directCic = cic;
|
||||||
receiverType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
receiverType = cic.getType().toString();
|
||||||
} else if (receiver instanceof MethodInvocation receiverMi) {
|
} else if (receiver instanceof MethodInvocation receiverMi) {
|
||||||
GetterChainReceiver chainedReceiver = resolveGetterChainToReceiver(receiverMi, scopeMethod);
|
Expression currentMi = receiverMi;
|
||||||
if (chainedReceiver != null) {
|
while (currentMi instanceof MethodInvocation chainMi) {
|
||||||
receiverType = chainedReceiver.typeFqn();
|
String mName = chainMi.getName().getIdentifier();
|
||||||
directCic = chainedReceiver.cic();
|
if (mName.equalsIgnoreCase("set" + propName) || mName.equalsIgnoreCase(propName)) {
|
||||||
} else {
|
if (!chainMi.arguments().isEmpty()) {
|
||||||
Expression currentMi = receiverMi;
|
Expression arg = (Expression) chainMi.arguments().get(0);
|
||||||
while (currentMi instanceof MethodInvocation chainMi) {
|
List<String> builderConstants = new ArrayList<>();
|
||||||
String mName = chainMi.getName().getIdentifier();
|
constantExtractor.extractConstantsFromExpression(arg, builderConstants);
|
||||||
if (mName.equalsIgnoreCase("set" + propName) || mName.equalsIgnoreCase(propName)) {
|
if (!builderConstants.isEmpty()) {
|
||||||
if (!chainMi.arguments().isEmpty()) {
|
return builderConstants;
|
||||||
Expression arg = (Expression) chainMi.arguments().get(0);
|
|
||||||
List<String> builderConstants = new ArrayList<>();
|
|
||||||
constantExtractor.extractConstantsFromExpression(arg, builderConstants);
|
|
||||||
if (!builderConstants.isEmpty()) {
|
|
||||||
return builderConstants;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentMi = chainMi.getExpression();
|
|
||||||
}
|
}
|
||||||
|
currentMi = chainMi.getExpression();
|
||||||
}
|
}
|
||||||
} else if (receiver instanceof SuperMethodInvocation) {
|
} else if (receiver instanceof SuperMethodInvocation) {
|
||||||
int lastDot = entryMethod.lastIndexOf('.');
|
int lastDot = entryMethod.lastIndexOf('.');
|
||||||
@@ -1357,14 +1082,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
if (currentTd != null) {
|
if (currentTd != null) {
|
||||||
receiverType = context.getSuperclassFqn(currentTd);
|
receiverType = context.getSuperclassFqn(currentTd);
|
||||||
}
|
}
|
||||||
} else if (receiver instanceof FieldAccess fa) {
|
|
||||||
receiverName = fa.getName().getIdentifier();
|
|
||||||
if (fa.resolveFieldBinding() != null && fa.resolveFieldBinding().getType() != null) {
|
|
||||||
receiverType = fa.resolveFieldBinding().getType().getErasure().getQualifiedName();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (receiverType == null && receiverName != null && path.size() > 1) {
|
if (receiverType == null && path.size() > 1) {
|
||||||
String callerMethod = path.get(1);
|
String callerMethod = path.get(1);
|
||||||
receiverType = variableTracer.getVariableDeclaredType(callerMethod, receiverName);
|
receiverType = variableTracer.getVariableDeclaredType(callerMethod, receiverName);
|
||||||
}
|
}
|
||||||
@@ -1403,7 +1123,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
if (directCic != null) {
|
if (directCic != null) {
|
||||||
cic = directCic;
|
cic = directCic;
|
||||||
} else {
|
} else {
|
||||||
Expression initExpr = findVariableInitializer(scopeMethod, varName);
|
Expression initExpr = findVariableInitializer(entryMethod, varName);
|
||||||
if (initExpr instanceof MethodInvocation initMi) {
|
if (initExpr instanceof MethodInvocation initMi) {
|
||||||
initExpr = unwrapMethodInvocation(initMi, 0, entryMethod);
|
initExpr = unwrapMethodInvocation(initMi, 0, entryMethod);
|
||||||
}
|
}
|
||||||
@@ -1411,55 +1131,46 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
cic = (ClassInstanceCreation) initExpr;
|
cic = (ClassInstanceCreation) initExpr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MethodDeclaration getter = null;
|
|
||||||
boolean anonymousGetter = false;
|
|
||||||
if (cic == null) {
|
if (cic == null) {
|
||||||
String ownerFqn = context.getFqn(td);
|
return null;
|
||||||
List<String> resolvedWithoutCic = accessorResolver.resolve(
|
}
|
||||||
ownerFqn,
|
MethodDeclaration getter = null;
|
||||||
getterName,
|
if (cic.getAnonymousClassDeclaration() != null) {
|
||||||
new AccessorResolver.ReceiverContext(td, null, contextCu, false, fieldValues),
|
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
|
||||||
ResolutionBudget.defaults());
|
if (declObj instanceof MethodDeclaration md && md.getName().getIdentifier().equals(getterName)) {
|
||||||
if (!resolvedWithoutCic.isEmpty()) {
|
getter = md;
|
||||||
return resolvedWithoutCic;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
if (cic.getAnonymousClassDeclaration() != null) {
|
if (getter == null) {
|
||||||
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
|
getter = context.findMethodDeclaration(td, getterName, true);
|
||||||
if (declObj instanceof MethodDeclaration md && md.getName().getIdentifier().equals(getterName)) {
|
}
|
||||||
getter = md;
|
if (getter != null && getter.getBody() != null) {
|
||||||
anonymousGetter = true;
|
fieldValues.putAll(constructorAnalyzer.buildFieldValuesFromCIC(td, cic, context, this::evaluateMethodCallInConstructor));
|
||||||
break;
|
int lastDotEntry = entryMethod.lastIndexOf('.');
|
||||||
|
if (lastDotEntry > 0) {
|
||||||
|
String className = entryMethod.substring(0, lastDotEntry);
|
||||||
|
String methodName = entryMethod.substring(lastDot + 1);
|
||||||
|
TypeDeclaration entryTd = context.getTypeDeclaration(className);
|
||||||
|
if (entryTd != null) {
|
||||||
|
MethodDeclaration entryMd = context.findMethodDeclaration(entryTd, methodName, false);
|
||||||
|
if (entryMd != null && entryMd.getBody() != null) {
|
||||||
|
// Skip trackSetterCallsBetween
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (getter == null) {
|
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, new HashSet<>());
|
||||||
getter = context.findMethodDeclaration(td, getterName, true);
|
if (result != null) {
|
||||||
}
|
String returnType = getter.getReturnType2() != null ? getter.getReturnType2().toString() : null;
|
||||||
if (getter != null && getter.getBody() != null) {
|
if (returnType != null && !result.contains(".")) {
|
||||||
fieldValues.putAll(constructorAnalyzer.buildFieldValuesFromCIC(
|
int lastDotRet = returnType.lastIndexOf('.');
|
||||||
td, cic, context, this::evaluateMethodCallInConstructor));
|
String simpleReturnType = lastDotRet > 0 ? returnType.substring(lastDotRet + 1) : returnType;
|
||||||
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, new HashSet<>());
|
if (!simpleReturnType.equals("String") && !simpleReturnType.equals("Integer") && !simpleReturnType.equals("Long") && !simpleReturnType.equals("Boolean")) {
|
||||||
if (result != null) {
|
result = simpleReturnType + "." + result;
|
||||||
String returnType = getter.getReturnType2() != null ? getter.getReturnType2().toString() : null;
|
|
||||||
if (returnType != null && !result.contains(".")) {
|
|
||||||
int lastDotRet = returnType.lastIndexOf('.');
|
|
||||||
String simpleReturnType = lastDotRet > 0 ? returnType.substring(lastDotRet + 1) : returnType;
|
|
||||||
if (!simpleReturnType.equals("String") && !simpleReturnType.equals("Integer")
|
|
||||||
&& !simpleReturnType.equals("Long") && !simpleReturnType.equals("Boolean")) {
|
|
||||||
result = simpleReturnType + "." + result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Collections.singletonList(result);
|
|
||||||
}
|
}
|
||||||
}
|
return Collections.singletonList(result);
|
||||||
List<String> resolvedWithCic = accessorResolver.resolve(
|
|
||||||
context.getFqn(td),
|
|
||||||
getterName,
|
|
||||||
new AccessorResolver.ReceiverContext(td, cic, contextCu, anonymousGetter, fieldValues),
|
|
||||||
ResolutionBudget.defaults());
|
|
||||||
if (!resolvedWithCic.isEmpty()) {
|
|
||||||
return resolvedWithCic;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1497,22 +1208,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return initExpr[0];
|
return initExpr[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Splits a traced argument into a receiver base and a trailing accessor suffix for getter-chain
|
|
||||||
* resolution, e.g. {@code wrapper.getEvent()} → {@code ["wrapper", ".getEvent()"]}.
|
|
||||||
* <p>
|
|
||||||
* Map/collection lookups ({@code ROUTES.get(key)}) stay intact and are resolved by
|
|
||||||
* {@link ConstantExtractor}; see {@link ExpressionAccessClassifier} for the type-based distinction
|
|
||||||
* from {@code Supplier.get()} and JavaBean accessors.
|
|
||||||
*/
|
|
||||||
protected String[] extractMethodSuffix(String paramName, String currentSuffix) {
|
protected String[] extractMethodSuffix(String paramName, String currentSuffix) {
|
||||||
return extractMethodSuffix(paramName, currentSuffix, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String[] extractMethodSuffix(String paramName, String currentSuffix, String scopeMethod) {
|
|
||||||
if (expressionAccessClassifier.isKeyedLookup(paramName, scopeMethod)) {
|
|
||||||
return new String[] { paramName, currentSuffix };
|
|
||||||
}
|
|
||||||
if (paramName != null && !paramName.contains(" ") && !paramName.startsWith("new ")) {
|
if (paramName != null && !paramName.contains(" ") && !paramName.startsWith("new ")) {
|
||||||
int bracketIndex = paramName.indexOf('[');
|
int bracketIndex = paramName.indexOf('[');
|
||||||
int dotIndex = -1;
|
int dotIndex = -1;
|
||||||
@@ -1547,21 +1243,90 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return new String[] { paramName, currentSuffix };
|
return new String[] { paramName, currentSuffix };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int getReturnedParameterIndex(MethodDeclaration md) {
|
||||||
|
if (md.getBody() == null) return -1;
|
||||||
|
List<?> parameters = md.parameters();
|
||||||
|
if (parameters.isEmpty()) return -1;
|
||||||
|
|
||||||
|
final List<String> returnedExprs = new ArrayList<>();
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(ReturnStatement node) {
|
||||||
|
if (node.getExpression() != null) {
|
||||||
|
returnedExprs.add(node.getExpression().toString());
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (returnedExprs.size() == 1) {
|
||||||
|
String retStr = returnedExprs.get(0);
|
||||||
|
for (int i = 0; i < parameters.size(); i++) {
|
||||||
|
if (parameters.get(i) instanceof SingleVariableDeclaration svd) {
|
||||||
|
if (svd.getName().getIdentifier().equals(retStr)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
protected Expression unwrapMethodInvocation(MethodInvocation mi, int depth) {
|
protected Expression unwrapMethodInvocation(MethodInvocation mi, int depth) {
|
||||||
return unwrapMethodInvocation(mi, depth, null);
|
return unwrapMethodInvocation(mi, depth, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Expression unwrapMethodInvocation(MethodInvocation mi, int depth, String methodFqn) {
|
protected Expression unwrapMethodInvocation(MethodInvocation mi, int depth, String methodFqn) {
|
||||||
return MethodInvocationUnwrapper.unwrap(mi, depth, methodFqn, context, miArg -> {
|
if (depth > 5) return mi;
|
||||||
if (methodFqn == null || !methodFqn.contains(".")) {
|
if (mi.getExpression() != null) {
|
||||||
return null;
|
String exprStr = mi.getExpression().toString();
|
||||||
|
if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects") && !exprStr.equals("Mono") && !exprStr.equals("Flux")) {
|
||||||
|
return mi;
|
||||||
}
|
}
|
||||||
String currentClass = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
} else {
|
||||||
if (miArg.getExpression() == null || miArg.getExpression().toString().equals("this")) {
|
if (methodFqn != null && methodFqn.contains(".")) {
|
||||||
return currentClass + "." + miArg.getName().getIdentifier();
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration localMd = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true);
|
||||||
|
if (localMd != null) {
|
||||||
|
int paramIdx = getReturnedParameterIndex(localMd);
|
||||||
|
if (paramIdx >= 0 && paramIdx < mi.arguments().size()) {
|
||||||
|
Expression arg = (Expression) mi.arguments().get(paramIdx);
|
||||||
|
if (arg instanceof MethodInvocation innerMi) {
|
||||||
|
return unwrapMethodInvocation(innerMi, depth + 1, methodFqn);
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return mi;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if (!mi.arguments().isEmpty()) {
|
||||||
|
String mName = mi.getName().getIdentifier();
|
||||||
|
String lowerName = mName.toLowerCase();
|
||||||
|
|
||||||
|
if (lowerName.startsWith("map") || lowerName.startsWith("parse") ||
|
||||||
|
lowerName.startsWith("convert") || lowerName.startsWith("to") ||
|
||||||
|
lowerName.startsWith("resolve") || lowerName.startsWith("build") ||
|
||||||
|
lowerName.startsWith("create") || lowerName.startsWith("from") ||
|
||||||
|
lowerName.startsWith("transform") || lowerName.startsWith("translate") ||
|
||||||
|
lowerName.startsWith("derive") || lowerName.startsWith("determine") ||
|
||||||
|
lowerName.startsWith("calculate") || lowerName.startsWith("decode") ||
|
||||||
|
lowerName.startsWith("extract") || lowerName.startsWith("get") ||
|
||||||
|
lowerName.startsWith("find")) {
|
||||||
|
return mi;
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression arg = (Expression) mi.arguments().get(0);
|
||||||
|
if (arg instanceof MethodInvocation innerMi) {
|
||||||
|
return unwrapMethodInvocation(innerMi, depth + 1, methodFqn);
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
return mi;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> resolveClassConstantReturns(String className, CodebaseContext context, CompilationUnit contextCu) {
|
public List<String> resolveClassConstantReturns(String className, CodebaseContext context, CompilationUnit contextCu) {
|
||||||
@@ -1826,22 +1591,12 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return allResolved;
|
return allResolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Peels enum factory wrappers while tracing call-path arguments, e.g.
|
|
||||||
* {@code OrderEvent.valueOf(action)} → {@code action} so the next hop can resolve the parameter.
|
|
||||||
* <p>
|
|
||||||
* Map/collection lookups ({@code ROUTES.get(key)}) are intentionally <em>not</em> peeled — they are
|
|
||||||
* routing tables, not enum converters. See {@link ExpressionAccessClassifier}.
|
|
||||||
*/
|
|
||||||
private String unwrapConverterMethods(String arg) {
|
private String unwrapConverterMethods(String arg) {
|
||||||
if (arg == null) return null;
|
if (arg == null) return null;
|
||||||
String current = arg;
|
String current = arg;
|
||||||
while (current.contains("(") && !current.startsWith("new ")) {
|
while (current.contains("(") && !current.startsWith("new ")) {
|
||||||
org.eclipse.jdt.core.dom.ASTNode argNode = parseExpressionString(current);
|
org.eclipse.jdt.core.dom.ASTNode argNode = parseExpressionString(current);
|
||||||
if (argNode instanceof org.eclipse.jdt.core.dom.MethodInvocation miArg && !miArg.arguments().isEmpty()) {
|
if (argNode instanceof org.eclipse.jdt.core.dom.MethodInvocation miArg && !miArg.arguments().isEmpty()) {
|
||||||
if (expressionAccessClassifier.isKeyedLookup(miArg, null)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
boolean shouldUnpack = false;
|
boolean shouldUnpack = false;
|
||||||
org.eclipse.jdt.core.dom.Expression recv = miArg.getExpression();
|
org.eclipse.jdt.core.dom.Expression recv = miArg.getExpression();
|
||||||
if (recv == null || recv instanceof org.eclipse.jdt.core.dom.ThisExpression) {
|
if (recv == null || recv instanceof org.eclipse.jdt.core.dom.ThisExpression) {
|
||||||
@@ -1864,118 +1619,4 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveValueOfEnumTypeName(MethodInvocation mi) {
|
|
||||||
if (mi.getExpression() instanceof SimpleName sn) {
|
|
||||||
return sn.getIdentifier();
|
|
||||||
}
|
|
||||||
if (mi.getExpression() instanceof QualifiedName qn) {
|
|
||||||
String full = qn.getFullyQualifiedName();
|
|
||||||
return full.contains(".") ? full.substring(full.lastIndexOf('.') + 1) : full;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveEnumConstantFromArgument(String argName, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
|
||||||
if (argName == null || path == null || path.size() < 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = path.size() - 1; i > 0; i--) {
|
|
||||||
String target = path.get(i);
|
|
||||||
String caller = path.get(i - 1);
|
|
||||||
|
|
||||||
Map<String, String> parameterValues = variableTracer.buildParameterValuesMap(caller, target, callGraph, path, i);
|
|
||||||
String traced = variableTracer.traceLocalVariable(target, argName, parameterValues);
|
|
||||||
if (traced != null && !traced.equals(argName)) {
|
|
||||||
String normalized = normalizeEnumConstantName(traced);
|
|
||||||
if (normalized != null) {
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int paramIndex = typeResolver.getParameterIndex(target, argName);
|
|
||||||
if (paramIndex >= 0) {
|
|
||||||
List<CallEdge> edges = callGraph.get(caller);
|
|
||||||
if (edges != null) {
|
|
||||||
for (CallEdge edge : edges) {
|
|
||||||
if (edge.getTargetMethod().equals(target) || pathFinder.isHeuristicMatch(edge.getTargetMethod(), target)) {
|
|
||||||
if (paramIndex < edge.getArguments().size()) {
|
|
||||||
String normalized = normalizeEnumConstantName(edge.getArguments().get(paramIndex));
|
|
||||||
if (normalized != null) {
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String normalizeEnumConstantName(String value) {
|
|
||||||
if (value == null || value.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String trimmed = value.trim();
|
|
||||||
if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
|
|
||||||
trimmed = trimmed.substring(1, trimmed.length() - 1);
|
|
||||||
}
|
|
||||||
if (trimmed.contains("(") || trimmed.contains(" ")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (trimmed.contains(".")) {
|
|
||||||
trimmed = trimmed.substring(trimmed.lastIndexOf('.') + 1);
|
|
||||||
}
|
|
||||||
if (!trimmed.matches("[A-Z_][A-Z0-9_]*")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasConcreteEnumConstants(List<String> polymorphicEvents, String declaredType, CodebaseContext context) {
|
|
||||||
if (polymorphicEvents == null || polymorphicEvents.isEmpty() || declaredType == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String simpleDeclared = declaredType.contains(".")
|
|
||||||
? declaredType.substring(declaredType.lastIndexOf('.') + 1)
|
|
||||||
: declaredType;
|
|
||||||
List<String> enumValues = context.getEnumValues(declaredType);
|
|
||||||
|
|
||||||
for (String pe : polymorphicEvents) {
|
|
||||||
if (pe == null || pe.startsWith("<SYMBOLIC:")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (pe.contains(".")) {
|
|
||||||
String typePart = pe.substring(0, pe.lastIndexOf('.'));
|
|
||||||
String simpleType = typePart.contains(".")
|
|
||||||
? typePart.substring(typePart.lastIndexOf('.') + 1)
|
|
||||||
: typePart;
|
|
||||||
if (simpleType.equals(simpleDeclared)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (enumValues != null && enumValues.contains(pe)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (enumValues != null) {
|
|
||||||
for (String ev : enumValues) {
|
|
||||||
if (ev.endsWith("." + pe)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isRuntimeEnumParameter(Expression exprNode) {
|
|
||||||
if (!(exprNode instanceof MethodInvocation mi)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!"valueOf".equals(mi.getName().getIdentifier()) || mi.arguments().size() != 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return mi.arguments().get(0) instanceof SimpleName;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.validation.AnalysisCanonicalFormValidator;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-applies machine-scoped enum canonicalization and validates the analysis model.
|
|
||||||
* Must run after property resolution so placeholders cannot bypass validation.
|
|
||||||
*/
|
|
||||||
public final class AnalysisResultFinalizer {
|
|
||||||
|
|
||||||
private AnalysisResultFinalizer() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void finalizeResult(AnalysisResult result, CodebaseContext context) {
|
|
||||||
if (result == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes = JsonExportContextFactory.resolveMachineTypes(
|
|
||||||
result.getName(),
|
|
||||||
result.getStateTypeFqn(),
|
|
||||||
result.getEventTypeFqn(),
|
|
||||||
context);
|
|
||||||
finalizeResult(result, context, machineTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void finalizeResult(
|
|
||||||
AnalysisResult result,
|
|
||||||
CodebaseContext context,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
|
||||||
if (result == null || machineTypes == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
persistMachineTypes(result, machineTypes);
|
|
||||||
applyCanonicalization(result, machineTypes);
|
|
||||||
|
|
||||||
if (context != null) {
|
|
||||||
AnalysisCanonicalFormValidator.enforce(result, context);
|
|
||||||
} else {
|
|
||||||
AnalysisCanonicalFormValidator.enforceWithMachineTypes(result, machineTypes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void persistMachineTypes(
|
|
||||||
AnalysisResult result,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
|
||||||
result.setStateTypeFqn(machineTypes.stateTypeFqn());
|
|
||||||
result.setEventTypeFqn(machineTypes.eventTypeFqn());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void applyCanonicalization(
|
|
||||||
AnalysisResult result,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
|
||||||
MachineEnumCanonicalizer.canonicalizeTransitions(result.getTransitions(), machineTypes);
|
|
||||||
|
|
||||||
if (result.getStates() != null) {
|
|
||||||
result.setStates(MachineEnumCanonicalizer.canonicalizeStates(
|
|
||||||
result.getStates(), machineTypes.stateTypeFqn()));
|
|
||||||
}
|
|
||||||
result.setStartStates(MachineEnumCanonicalizer.canonicalizeStateLabels(
|
|
||||||
result.getStartStates(), machineTypes.stateTypeFqn()));
|
|
||||||
result.setEndStates(MachineEnumCanonicalizer.canonicalizeStateLabels(
|
|
||||||
result.getEndStates(), machineTypes.stateTypeFqn()));
|
|
||||||
|
|
||||||
if (result.getMetadata() != null) {
|
|
||||||
CodebaseMetadata metadata = result.getMetadata();
|
|
||||||
List<TriggerPoint> triggers = canonicalizeTriggers(metadata.getTriggers(), machineTypes);
|
|
||||||
List<CallChain> callChains = canonicalizeCallChains(metadata.getCallChains(), machineTypes);
|
|
||||||
result.setMetadata(CodebaseMetadata.builder()
|
|
||||||
.triggers(triggers)
|
|
||||||
.entryPoints(metadata.getEntryPoints())
|
|
||||||
.callChains(callChains)
|
|
||||||
.properties(metadata.getProperties())
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<TriggerPoint> canonicalizeTriggers(
|
|
||||||
List<TriggerPoint> triggers,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
|
||||||
if (triggers == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return triggers.stream()
|
|
||||||
.map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<CallChain> canonicalizeCallChains(
|
|
||||||
List<CallChain> callChains,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
|
||||||
if (callChains == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return callChains.stream()
|
|
||||||
.map(chain -> {
|
|
||||||
TriggerPoint trigger = chain.getTriggerPoint() == null
|
|
||||||
? null
|
|
||||||
: MachineEnumCanonicalizer.canonicalizeTriggerPoint(chain.getTriggerPoint(), machineTypes);
|
|
||||||
List<MatchedTransition> matched = canonicalizeMatchedTransitions(
|
|
||||||
chain.getMatchedTransitions(), machineTypes);
|
|
||||||
return chain.toBuilder()
|
|
||||||
.triggerPoint(trigger)
|
|
||||||
.matchedTransitions(matched)
|
|
||||||
.build();
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<MatchedTransition> canonicalizeMatchedTransitions(
|
|
||||||
List<MatchedTransition> matchedTransitions,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
|
||||||
if (matchedTransitions == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return matchedTransitions.stream()
|
|
||||||
.map(matched -> MatchedTransition.builder()
|
|
||||||
.event(MachineEnumCanonicalizer.canonicalizeLabel(
|
|
||||||
matched.getEvent(), machineTypes.eventTypeFqn()))
|
|
||||||
.sourceState(MachineEnumCanonicalizer.canonicalizeLabel(
|
|
||||||
matched.getSourceState(), machineTypes.stateTypeFqn()))
|
|
||||||
.targetState(MachineEnumCanonicalizer.canonicalizeLabel(
|
|
||||||
matched.getTargetState(), machineTypes.stateTypeFqn()))
|
|
||||||
.build())
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,4 +8,8 @@ import java.util.List;
|
|||||||
|
|
||||||
public interface CallGraphEngine {
|
public interface CallGraphEngine {
|
||||||
List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers);
|
List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers);
|
||||||
|
|
||||||
|
default java.util.Map<String, java.util.List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> getCallGraph() {
|
||||||
|
return java.util.Collections.emptyMap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,21 +56,6 @@ public class CallGraphPathFinder {
|
|||||||
return findAllPaths(start, target, graph, visited, new boolean[]{false});
|
return findAllPaths(start, target, graph, visited, new boolean[]{false});
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<List<String>> findAllPaths(
|
|
||||||
String start,
|
|
||||||
String target,
|
|
||||||
Map<String, List<CallEdge>> graph,
|
|
||||||
Set<String> visited,
|
|
||||||
PathBindingEvaluator bindingEvaluator,
|
|
||||||
Map<String, String> initialBindings) {
|
|
||||||
if (bindingEvaluator == null) {
|
|
||||||
return findAllPaths(start, target, graph, visited);
|
|
||||||
}
|
|
||||||
Map<String, String> bindings = initialBindings == null ? new HashMap<>() : new HashMap<>(initialBindings);
|
|
||||||
return findAllPathsConstrained(
|
|
||||||
start, target, graph, visited, new boolean[]{false}, bindingEvaluator, bindings);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<List<String>> findAllPaths(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited, boolean[] parentHitCycle) {
|
public List<List<String>> findAllPaths(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited, boolean[] parentHitCycle) {
|
||||||
List<List<String>> result = new ArrayList<>();
|
List<List<String>> result = new ArrayList<>();
|
||||||
java.util.Map.Entry<String, String> cacheKey = new java.util.AbstractMap.SimpleImmutableEntry<>(start, target);
|
java.util.Map.Entry<String, String> cacheKey = new java.util.AbstractMap.SimpleImmutableEntry<>(start, target);
|
||||||
@@ -190,121 +175,6 @@ public class CallGraphPathFinder {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<List<String>> findAllPathsConstrained(
|
|
||||||
String start,
|
|
||||||
String target,
|
|
||||||
Map<String, List<CallEdge>> graph,
|
|
||||||
Set<String> visited,
|
|
||||||
boolean[] parentHitCycle,
|
|
||||||
PathBindingEvaluator bindingEvaluator,
|
|
||||||
Map<String, String> bindings) {
|
|
||||||
List<List<String>> result = new ArrayList<>();
|
|
||||||
if (start.equals(target)) {
|
|
||||||
result.add(new ArrayList<>(List.of(start)));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (!visited.add(start)) {
|
|
||||||
parentHitCycle[0] = true;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<CallEdge> neighbors = graph.get(start);
|
|
||||||
if (neighbors != null) {
|
|
||||||
for (CallEdge edge : neighbors) {
|
|
||||||
String neighbor = edge.getTargetMethod();
|
|
||||||
if (neighbor.startsWith("java.") || neighbor.startsWith("javax.") || neighbor.startsWith("jakarta.") || neighbor.startsWith("org.springframework.")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String resolvedTarget = neighbor.equals(target) || isHeuristicMatch(neighbor, target) ? target : neighbor;
|
|
||||||
List<String> hopPath = new ArrayList<>(List.of(start, resolvedTarget));
|
|
||||||
Map<String, String> branchBindings = new HashMap<>(bindings);
|
|
||||||
if (!bindingEvaluator.tryTraverseEdge(
|
|
||||||
start, resolvedTarget, edge, hopPath, 1, graph, this, branchBindings)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolvedTarget.equals(target)) {
|
|
||||||
result.add(hopPath);
|
|
||||||
} else {
|
|
||||||
List<List<String>> subPaths = findAllPathsConstrained(
|
|
||||||
neighbor, target, graph, visited, parentHitCycle, bindingEvaluator, branchBindings);
|
|
||||||
for (List<String> subPath : subPaths) {
|
|
||||||
if (subPath.isEmpty() || subPath.get(0).equals(start)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
List<String> path = new ArrayList<>();
|
|
||||||
path.add(start);
|
|
||||||
path.addAll(subPath);
|
|
||||||
result.add(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.isEmpty() && start.contains(".")) {
|
|
||||||
result.addAll(findInheritanceFallbackPaths(
|
|
||||||
start, target, graph, visited, parentHitCycle, bindingEvaluator, bindings));
|
|
||||||
}
|
|
||||||
|
|
||||||
visited.remove(start);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<List<String>> findInheritanceFallbackPaths(
|
|
||||||
String start,
|
|
||||||
String target,
|
|
||||||
Map<String, List<CallEdge>> graph,
|
|
||||||
Set<String> visited,
|
|
||||||
boolean[] parentHitCycle,
|
|
||||||
PathBindingEvaluator bindingEvaluator,
|
|
||||||
Map<String, String> bindings) {
|
|
||||||
List<List<String>> result = new ArrayList<>();
|
|
||||||
String className = start.substring(0, start.lastIndexOf('.'));
|
|
||||||
String methodName = start.substring(start.lastIndexOf('.') + 1);
|
|
||||||
if (className.contains("<")) {
|
|
||||||
className = className.substring(0, className.indexOf('<'));
|
|
||||||
}
|
|
||||||
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
|
||||||
if (td != null) {
|
|
||||||
String superclass = context.getSuperclassFqn(td);
|
|
||||||
if (superclass != null) {
|
|
||||||
String superMethod = superclass + "." + methodName;
|
|
||||||
if (graph.containsKey(superMethod)) {
|
|
||||||
List<List<String>> superPaths = findAllPathsConstrained(
|
|
||||||
superMethod, target, graph, visited, parentHitCycle, bindingEvaluator, bindings);
|
|
||||||
for (List<String> superPath : superPaths) {
|
|
||||||
List<String> path = new ArrayList<>();
|
|
||||||
path.add(start);
|
|
||||||
path.addAll(superPath);
|
|
||||||
result.add(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.isEmpty()) {
|
|
||||||
List<String> impls = context.getImplementations(className);
|
|
||||||
if (impls != null) {
|
|
||||||
for (String impl : impls) {
|
|
||||||
String implMethod = impl + "." + methodName;
|
|
||||||
if (graph.containsKey(implMethod)) {
|
|
||||||
List<List<String>> implPaths = findAllPathsConstrained(
|
|
||||||
implMethod, target, graph, visited, parentHitCycle, bindingEvaluator, bindings);
|
|
||||||
for (List<String> implPath : implPaths) {
|
|
||||||
List<String> path = new ArrayList<>();
|
|
||||||
path.add(start);
|
|
||||||
path.addAll(implPath);
|
|
||||||
result.add(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String extractContextMachineId(List<String> path, Map<String, List<CallEdge>> callGraph) {
|
public String extractContextMachineId(List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||||
for (String node : path) {
|
for (String node : path) {
|
||||||
List<CallEdge> edges = callGraph.get(node);
|
List<CallEdge> edges = callGraph.get(node);
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class CompositeCallGraphEngine implements CallGraphEngine {
|
||||||
|
private final List<CallGraphEngine> engines;
|
||||||
|
private final Map<String, List<CallEdge>> mergedGraph = new HashMap<>();
|
||||||
|
|
||||||
|
public CompositeCallGraphEngine(List<CallGraphEngine> engines) {
|
||||||
|
this.engines = engines;
|
||||||
|
mergeGraphs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mergeGraphs() {
|
||||||
|
for (CallGraphEngine engine : engines) {
|
||||||
|
Map<String, List<CallEdge>> graph = engine.getCallGraph();
|
||||||
|
if (graph != null) {
|
||||||
|
for (Map.Entry<String, List<CallEdge>> entry : graph.entrySet()) {
|
||||||
|
String caller = normalizeCompanionFqn(entry.getKey());
|
||||||
|
List<CallEdge> edges = mergedGraph.computeIfAbsent(caller, k -> new ArrayList<>());
|
||||||
|
for (CallEdge edge : entry.getValue()) {
|
||||||
|
String target = normalizeCompanionFqn(edge.getTargetMethod());
|
||||||
|
edges.add(new CallEdge(target, edge.getArguments(), edge.getReceiver()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeCompanionFqn(String fqn) {
|
||||||
|
if (fqn == null) return null;
|
||||||
|
return fqn.replace(".Companion.", ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, List<CallEdge>> getCallGraph() {
|
||||||
|
return mergedGraph;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||||
|
List<CallChain> chains = new ArrayList<>();
|
||||||
|
for (EntryPoint ep : entryPoints) {
|
||||||
|
String startMethod = normalizeCompanionFqn(ep.getClassName() + "." + ep.getMethodName());
|
||||||
|
for (TriggerPoint tp : triggers) {
|
||||||
|
String targetMethod = normalizeCompanionFqn(tp.getClassName() + "." + tp.getMethodName());
|
||||||
|
List<List<String>> paths = findAllPaths(startMethod, targetMethod, new LinkedHashSet<>());
|
||||||
|
for (List<String> path : paths) {
|
||||||
|
chains.add(CallChain.builder()
|
||||||
|
.entryPoint(ep)
|
||||||
|
.methodChain(path)
|
||||||
|
.triggerPoint(tp)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chains;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<List<String>> findAllPaths(String start, String target, Set<String> visited) {
|
||||||
|
List<List<String>> result = new ArrayList<>();
|
||||||
|
if (start.equals(target)) {
|
||||||
|
result.add(new ArrayList<>(List.of(start)));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (!visited.add(start)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CallEdge> neighbors = mergedGraph.get(start);
|
||||||
|
if (neighbors != null) {
|
||||||
|
for (CallEdge edge : neighbors) {
|
||||||
|
String neighbor = edge.getTargetMethod();
|
||||||
|
if (neighbor.startsWith("java.") || neighbor.startsWith("javax.") ||
|
||||||
|
neighbor.startsWith("jakarta.") || neighbor.startsWith("org.springframework.")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
|
||||||
|
List<String> path = new ArrayList<>(List.of(start, target));
|
||||||
|
result.add(path);
|
||||||
|
} else {
|
||||||
|
List<List<String>> subPaths = findAllPaths(neighbor, target, visited);
|
||||||
|
for (List<String> subPath : subPaths) {
|
||||||
|
List<String> path = new ArrayList<>();
|
||||||
|
path.add(start);
|
||||||
|
path.addAll(subPath);
|
||||||
|
result.add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visited.remove(start);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isHeuristicMatch(String neighbor, String target) {
|
||||||
|
if (neighbor == null || target == null) return false;
|
||||||
|
if (neighbor.equals(target)) return true;
|
||||||
|
if (neighbor.contains(".") && target.contains(".")) {
|
||||||
|
String methodNeighbor = neighbor.substring(neighbor.lastIndexOf('.') + 1);
|
||||||
|
String methodTarget = target.substring(target.lastIndexOf('.') + 1);
|
||||||
|
if (methodNeighbor.equals(methodTarget)) {
|
||||||
|
String classNeighbor = neighbor.substring(0, neighbor.lastIndexOf('.'));
|
||||||
|
String classTarget = target.substring(0, target.lastIndexOf('.'));
|
||||||
|
String simpleClassNeighbor = classNeighbor.contains(".") ? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor;
|
||||||
|
String simpleClassTarget = classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget;
|
||||||
|
return simpleClassNeighbor.equalsIgnoreCase(simpleClassTarget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class CompositeIntelligenceProvider implements CodebaseIntelligenceProvider {
|
||||||
|
private final List<CodebaseIntelligenceProvider> providers;
|
||||||
|
private final List<CallGraphEngine> engines;
|
||||||
|
|
||||||
|
public CompositeIntelligenceProvider(List<CodebaseIntelligenceProvider> providers, List<CallGraphEngine> engines) {
|
||||||
|
this.providers = providers;
|
||||||
|
this.engines = engines;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<TriggerPoint> findTriggerPoints() {
|
||||||
|
List<TriggerPoint> result = new ArrayList<>();
|
||||||
|
for (CodebaseIntelligenceProvider provider : providers) {
|
||||||
|
result.addAll(provider.findTriggerPoints());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<EntryPoint> findEntryPoints() {
|
||||||
|
List<EntryPoint> result = new ArrayList<>();
|
||||||
|
for (CodebaseIntelligenceProvider provider : providers) {
|
||||||
|
result.addAll(provider.findEntryPoints());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||||
|
CompositeCallGraphEngine compositeEngine = new CompositeCallGraphEngine(engines);
|
||||||
|
return compositeEngine.findChains(entryPoints, triggers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Map<String, String>> resolveProperties() {
|
||||||
|
Map<String, Map<String, String>> result = new HashMap<>();
|
||||||
|
for (CodebaseIntelligenceProvider provider : providers) {
|
||||||
|
Map<String, Map<String, String>> props = provider.resolveProperties();
|
||||||
|
if (props != null) {
|
||||||
|
result.putAll(props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorFieldResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.AccessorResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.LibraryUnwrapRegistry;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -19,8 +14,6 @@ public class ConstantExtractor {
|
|||||||
private final Function<MethodInvocation, String> calledMethodResolver;
|
private final Function<MethodInvocation, String> calledMethodResolver;
|
||||||
private VariableTracer variableTracer;
|
private VariableTracer variableTracer;
|
||||||
private ConstructorAnalyzer constructorAnalyzer;
|
private ConstructorAnalyzer constructorAnalyzer;
|
||||||
private ExpressionAccessClassifier expressionAccessClassifier;
|
|
||||||
private AccessorResolver accessorResolver;
|
|
||||||
|
|
||||||
public ConstantExtractor(CodebaseContext context, ConstantResolver constantResolver, Function<MethodInvocation, String> calledMethodResolver) {
|
public ConstantExtractor(CodebaseContext context, ConstantResolver constantResolver, Function<MethodInvocation, String> calledMethodResolver) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
@@ -36,24 +29,25 @@ public class ConstantExtractor {
|
|||||||
this.constructorAnalyzer = constructorAnalyzer;
|
this.constructorAnalyzer = constructorAnalyzer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setExpressionAccessClassifier(ExpressionAccessClassifier expressionAccessClassifier) {
|
|
||||||
this.expressionAccessClassifier = expressionAccessClassifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAccessorResolver(AccessorResolver accessorResolver) {
|
|
||||||
this.accessorResolver = accessorResolver;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void extractConstantsFromExpression(Expression expr, List<String> constants) {
|
public void extractConstantsFromExpression(Expression expr, List<String> constants) {
|
||||||
if (constantResolver != null) {
|
if (constantResolver != null) {
|
||||||
String resolved = constantResolver.resolve(expr, context);
|
String resolved = constantResolver.resolve(expr, context);
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
addResolvedConstant(resolved, constants);
|
if (resolved.startsWith("ENUM_SET:")) {
|
||||||
|
for (String eVal : resolved.substring(9).split(",")) {
|
||||||
|
String parsed = parseEnumSetElement(eVal);
|
||||||
|
if (!constants.contains(parsed)) constants.add(parsed);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
constants.add(resolved);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expr instanceof StringLiteral sl) {
|
if (expr instanceof QualifiedName qn) {
|
||||||
|
constants.add(qn.toString());
|
||||||
|
} else if (expr instanceof StringLiteral sl) {
|
||||||
constants.add(sl.getLiteralValue());
|
constants.add(sl.getLiteralValue());
|
||||||
} else if (expr instanceof ConditionalExpression ce) {
|
} else if (expr instanceof ConditionalExpression ce) {
|
||||||
extractConstantsFromExpression(ce.getThenExpression(), constants);
|
extractConstantsFromExpression(ce.getThenExpression(), constants);
|
||||||
@@ -96,17 +90,21 @@ public class ConstantExtractor {
|
|||||||
String methodName = mi.getName().getIdentifier();
|
String methodName = mi.getName().getIdentifier();
|
||||||
|
|
||||||
log.trace("Processing mi: {}", mi);
|
log.trace("Processing mi: {}", mi);
|
||||||
if (("get".equals(methodName) || "getOrDefault".equals(methodName)) && mi.getExpression() != null) {
|
if (methodName.equals("get") || methodName.equals("getOrDefault")) {
|
||||||
if (isKeyedLookupMethod(mi)) {
|
if (mi.getExpression() != null) {
|
||||||
extractMapReceiverConstants(mi.getExpression(), constants);
|
if (variableTracer != null) {
|
||||||
return;
|
List<Expression> traced = variableTracer.traceVariableAll(mi.getExpression());
|
||||||
}
|
for (Expression t : traced) {
|
||||||
if ("get".equals(methodName) && mi.arguments().isEmpty()) {
|
extractConstantsFromExpression(t, constants);
|
||||||
extractMapReceiverConstants(mi.getExpression(), constants);
|
}
|
||||||
return;
|
} else {
|
||||||
|
extractConstantsFromExpression(mi.getExpression(), constants);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (LibraryUnwrapRegistry.isUnwrapMethodName(methodName)) {
|
if (methodName.equals("of") || methodName.equals("ofEntries") || methodName.equals("asList") || methodName.equals("entry") ||
|
||||||
|
methodName.equals("just") || methodName.equals("withPayload") || methodName.equals("success")) {
|
||||||
for (Object argObj : mi.arguments()) {
|
for (Object argObj : mi.arguments()) {
|
||||||
extractConstantsFromExpression((Expression) argObj, constants);
|
extractConstantsFromExpression((Expression) argObj, constants);
|
||||||
}
|
}
|
||||||
@@ -217,9 +215,9 @@ public class ConstantExtractor {
|
|||||||
Expression currentMi = mi;
|
Expression currentMi = mi;
|
||||||
while (currentMi instanceof MethodInvocation chainMi) {
|
while (currentMi instanceof MethodInvocation chainMi) {
|
||||||
String chainName = chainMi.getName().getIdentifier();
|
String chainName = chainMi.getName().getIdentifier();
|
||||||
if (LibraryUnwrapRegistry.isHeaderSetterMethodName(chainName) && chainMi.arguments().size() == 2) {
|
if (chainName.equals("setHeader") && chainMi.arguments().size() == 2) {
|
||||||
extractConstantsFromExpression((Expression) chainMi.arguments().get(1), constants);
|
extractConstantsFromExpression((Expression) chainMi.arguments().get(1), constants);
|
||||||
} else if (LibraryUnwrapRegistry.isEventSetterMethodName(chainName) && chainMi.arguments().size() == 1) {
|
} else if ((chainName.equalsIgnoreCase("event") || chainName.equalsIgnoreCase("type") || chainName.equalsIgnoreCase("eventType")) && chainMi.arguments().size() == 1) {
|
||||||
extractConstantsFromExpression((Expression) chainMi.arguments().get(0), constants);
|
extractConstantsFromExpression((Expression) chainMi.arguments().get(0), constants);
|
||||||
}
|
}
|
||||||
currentMi = chainMi.getExpression();
|
currentMi = chainMi.getExpression();
|
||||||
@@ -284,23 +282,9 @@ public class ConstantExtractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<String> resolveMethodReturnConstant(String className, String methodName, int depth, Set<String> visited, CompilationUnit contextCu) {
|
public List<String> resolveMethodReturnConstant(String className, String methodName, int depth, Set<String> visited, CompilationUnit contextCu) {
|
||||||
return resolveMethodReturnConstant(className, methodName, depth, visited, contextCu, ResolutionBudget.defaults());
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> resolveMethodReturnConstant(
|
|
||||||
String className,
|
|
||||||
String methodName,
|
|
||||||
int depth,
|
|
||||||
Set<String> visited,
|
|
||||||
CompilationUnit contextCu,
|
|
||||||
ResolutionBudget budget) {
|
|
||||||
log.debug("ConstantExtractor.resolveMethodReturnConstant CALLED: className={}, methodName={}", className, methodName);
|
log.debug("ConstantExtractor.resolveMethodReturnConstant CALLED: className={}, methodName={}", className, methodName);
|
||||||
log.debug("Entering resolveMethodReturnConstant with className={}, methodName={}, depth={}", className, methodName, depth);
|
log.debug("Entering resolveMethodReturnConstant with className={}, methodName={}, depth={}", className, methodName, depth);
|
||||||
if (budget == null) {
|
if (depth > 20) {
|
||||||
budget = ResolutionBudget.defaults();
|
|
||||||
}
|
|
||||||
final ResolutionBudget activeBudget = budget;
|
|
||||||
if (activeBudget.isMethodReturnExhausted(depth)) {
|
|
||||||
log.debug("Exiting resolveMethodReturnConstant early (depth limit) for {}.{}", className, methodName);
|
log.debug("Exiting resolveMethodReturnConstant early (depth limit) for {}.{}", className, methodName);
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
@@ -310,34 +294,12 @@ public class ConstantExtractor {
|
|||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(className, methodName, activeBudget);
|
List<String> constants = new ArrayList<>();
|
||||||
TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null;
|
TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null;
|
||||||
if (tempTd == null) {
|
if (tempTd == null) {
|
||||||
tempTd = context.getTypeDeclaration(className);
|
tempTd = context.getTypeDeclaration(className);
|
||||||
}
|
}
|
||||||
final TypeDeclaration td = tempTd;
|
final TypeDeclaration td = tempTd;
|
||||||
boolean widenToImplementations = td == null
|
|
||||||
|| td.isInterface()
|
|
||||||
|| Modifier.isAbstract(td.getModifiers());
|
|
||||||
|
|
||||||
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter() && !widenToImplementations) {
|
|
||||||
if (indexedAccessor.get().kind() == click.kamil.springstatemachineexporter.analysis.index.AccessorKind.CONSTANT_GETTER) {
|
|
||||||
List<String> constantGetterValues = extractConstantGetterReturn(
|
|
||||||
indexedAccessor.get().ownerFqn(), methodName, contextCu, visited);
|
|
||||||
if (!constantGetterValues.isEmpty()) {
|
|
||||||
visited.remove(fqn);
|
|
||||||
return constantGetterValues;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
List<String> indexedConstants = resolveIndexedGetterFieldConstants(
|
|
||||||
indexedAccessor.get(), contextCu, visited);
|
|
||||||
if (!indexedConstants.isEmpty()) {
|
|
||||||
visited.remove(fqn);
|
|
||||||
return indexedConstants;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> constants = new ArrayList<>();
|
|
||||||
if (td != null) {
|
if (td != null) {
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
if (md != null && md.getBody() != null) {
|
if (md != null && md.getBody() != null) {
|
||||||
@@ -360,8 +322,7 @@ public class ConstantExtractor {
|
|||||||
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
|
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
|
||||||
|
|
||||||
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
|
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
|
||||||
List<String> delegationResult = resolveMethodReturnConstant(
|
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
|
||||||
cName, mName, depth + 1, visited, contextCu, activeBudget);
|
|
||||||
constants.addAll(delegationResult);
|
constants.addAll(delegationResult);
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
@@ -380,8 +341,7 @@ public class ConstantExtractor {
|
|||||||
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
|
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
|
||||||
|
|
||||||
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
|
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
|
||||||
List<String> delegationResult = resolveMethodReturnConstant(
|
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
|
||||||
cName, mName, depth + 1, visited, contextCu, activeBudget);
|
|
||||||
constants.addAll(delegationResult);
|
constants.addAll(delegationResult);
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
@@ -422,13 +382,11 @@ public class ConstantExtractor {
|
|||||||
return super.visit(node);
|
return super.visit(node);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
} else {
|
||||||
if (constants.isEmpty() && widenToImplementations) {
|
|
||||||
List<String> impls = context.getImplementations(className);
|
List<String> impls = context.getImplementations(className);
|
||||||
if (impls != null) {
|
if (impls != null) {
|
||||||
for (String implName : impls) {
|
for (String implName : impls) {
|
||||||
List<String> delegationResult = resolveMethodReturnConstant(
|
List<String> delegationResult = resolveMethodReturnConstant(implName, methodName, depth + 1, visited, contextCu);
|
||||||
implName, methodName, depth + 1, visited, contextCu, activeBudget);
|
|
||||||
constants.addAll(delegationResult);
|
constants.addAll(delegationResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -440,170 +398,10 @@ public class ConstantExtractor {
|
|||||||
return constants;
|
return constants;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> resolveIndexedGetterFieldConstants(
|
|
||||||
AccessorSummary accessor,
|
|
||||||
CompilationUnit contextCu,
|
|
||||||
Set<String> visited) {
|
|
||||||
return AccessorFieldResolver.resolveFieldConstants(
|
|
||||||
accessor,
|
|
||||||
context,
|
|
||||||
constructorAnalyzer,
|
|
||||||
contextCu,
|
|
||||||
visited,
|
|
||||||
this::extractConstantsFromExpression);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<AccessorSummary> lookupIndexedGetter(String className, String methodName) {
|
|
||||||
return lookupIndexedGetter(className, methodName, ResolutionBudget.defaults());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<AccessorSummary> lookupIndexedGetter(String className, String methodName, ResolutionBudget budget) {
|
|
||||||
if (accessorResolver != null) {
|
|
||||||
return accessorResolver.lookupIndexedGetter(className, methodName);
|
|
||||||
}
|
|
||||||
if (className == null || className.isBlank()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Optional<AccessorSummary> direct = context.getAccessorIndex().lookup(className, methodName);
|
|
||||||
if (direct.isPresent()) {
|
|
||||||
return direct;
|
|
||||||
}
|
|
||||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(className);
|
|
||||||
if (typeDeclaration != null) {
|
|
||||||
direct = context.getAccessorIndex().lookup(context.getFqn(typeDeclaration), methodName);
|
|
||||||
if (direct.isPresent()) {
|
|
||||||
return direct;
|
|
||||||
}
|
|
||||||
if (!typeDeclaration.isInterface() && !Modifier.isAbstract(typeDeclaration.getModifiers())) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
List<String> implementations = context.getImplementations(className);
|
|
||||||
if (implementations == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
for (String implementation : implementations) {
|
|
||||||
Optional<AccessorSummary> implAccessor = context.getAccessorIndex().lookup(implementation, methodName);
|
|
||||||
if (implAccessor.isPresent()) {
|
|
||||||
return implAccessor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> extractConstantGetterReturn(
|
|
||||||
String ownerFqn,
|
|
||||||
String methodName,
|
|
||||||
CompilationUnit contextCu,
|
|
||||||
Set<String> visited) {
|
|
||||||
TypeDeclaration typeDeclaration = contextCu != null
|
|
||||||
? context.getTypeDeclaration(ownerFqn, contextCu)
|
|
||||||
: null;
|
|
||||||
if (typeDeclaration == null) {
|
|
||||||
typeDeclaration = context.getTypeDeclaration(ownerFqn);
|
|
||||||
}
|
|
||||||
if (typeDeclaration == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
MethodDeclaration methodDeclaration = context.findMethodDeclaration(typeDeclaration, methodName, true);
|
|
||||||
if (methodDeclaration == null || methodDeclaration.getBody() == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
List<String> constants = new ArrayList<>();
|
|
||||||
methodDeclaration.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(ReturnStatement node) {
|
|
||||||
if (node.getExpression() != null) {
|
|
||||||
extractConstantsFromExpression(node.getExpression(), constants);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return constants;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String parseEnumSetElement(String eVal) {
|
public String parseEnumSetElement(String eVal) {
|
||||||
return eVal.contains(".") ? eVal.substring(eVal.lastIndexOf('.', eVal.lastIndexOf('.') - 1) + 1) : eVal;
|
return eVal.contains(".") ? eVal.substring(eVal.lastIndexOf('.', eVal.lastIndexOf('.') - 1) + 1) : eVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isKeyedLookupMethod(MethodInvocation mi) {
|
|
||||||
if (expressionAccessClassifier != null) {
|
|
||||||
return expressionAccessClassifier.isKeyedLookup(mi, null);
|
|
||||||
}
|
|
||||||
return "getOrDefault".equals(mi.getName().getIdentifier())
|
|
||||||
|| ("get".equals(mi.getName().getIdentifier()) && !mi.arguments().isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves constants from the map/collection receiver of {@code .get(key)}.
|
|
||||||
* Uses {@link ConstantResolver} for static fields ({@code ROUTES = Map.of(...)}) first;
|
|
||||||
* falls back to dataflow tracing for locals ({@code Map map = ...; map.get(k)}).
|
|
||||||
*/
|
|
||||||
private void extractMapReceiverConstants(Expression receiver, List<String> constants) {
|
|
||||||
if (tryAddResolvedExpression(receiver, constants)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (receiver instanceof MethodInvocation || receiver instanceof ArrayCreation || receiver instanceof ArrayInitializer) {
|
|
||||||
extractConstantsFromExpression(receiver, constants);
|
|
||||||
if (!constants.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (variableTracer == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String receiverText = receiver.toString();
|
|
||||||
for (Expression traced : variableTracer.traceVariableAll(receiver)) {
|
|
||||||
if (traced == null || receiverText.equals(traced.toString())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (tryAddResolvedExpression(traced, constants)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
extractConstantsFromExpression(traced, constants);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean tryAddResolvedExpression(Expression expr, List<String> constants) {
|
|
||||||
if (constantResolver == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String resolved = constantResolver.resolve(expr, context);
|
|
||||||
if (resolved == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
addResolvedConstant(resolved, constants);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addResolvedConstant(String resolved, List<String> constants) {
|
|
||||||
if (resolved.startsWith("ENUM_SET:")) {
|
|
||||||
for (String eVal : resolved.substring(9).split(",")) {
|
|
||||||
String parsed = parseEnumSetElement(eVal);
|
|
||||||
if (!constants.contains(parsed)) {
|
|
||||||
constants.add(parsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (resolved.startsWith("MAP:")) {
|
|
||||||
List<String> mapValues = ConstantResolver.decodeMapLiteralValues(resolved);
|
|
||||||
if (mapValues != null) {
|
|
||||||
for (String mapValue : mapValues) {
|
|
||||||
if (!constants.contains(mapValue)) {
|
|
||||||
constants.add(mapValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (resolved.startsWith("ARRAY:")) {
|
|
||||||
for (String arrayValue : resolved.substring(6).split("\\|", -1)) {
|
|
||||||
if (!constants.contains(arrayValue)) {
|
|
||||||
constants.add(arrayValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (!constants.contains(resolved)) {
|
|
||||||
constants.add(resolved);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Block findEnclosingBlock(ASTNode node) {
|
private Block findEnclosingBlock(ASTNode node) {
|
||||||
ASTNode parent = node.getParent();
|
ASTNode parent = node.getParent();
|
||||||
while (parent != null && !(parent instanceof Block)) {
|
while (parent != null && !(parent instanceof Block)) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import org.eclipse.jdt.core.dom.*;
|
import org.eclipse.jdt.core.dom.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -34,15 +33,6 @@ public class ConstructorAnalyzer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
|
public List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
|
||||||
return traceFieldInConstructors(td, fieldName, context, visited, ResolutionBudget.defaults());
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> traceFieldInConstructors(
|
|
||||||
TypeDeclaration td,
|
|
||||||
String fieldName,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
ResolutionBudget budget) {
|
|
||||||
if (td == null || fieldName == null) return Collections.emptyList();
|
if (td == null || fieldName == null) return Collections.emptyList();
|
||||||
String fqn = context.getFqn(td);
|
String fqn = context.getFqn(td);
|
||||||
if (fqn == null) return Collections.emptyList();
|
if (fqn == null) return Collections.emptyList();
|
||||||
@@ -72,8 +62,7 @@ public class ConstructorAnalyzer {
|
|||||||
targetClass = context.getFqn(targetTd);
|
targetClass = context.getFqn(targetTd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.addAll(constantExtractor.resolveMethodReturnConstant(
|
results.addAll(constantExtractor.resolveMethodReturnConstant(targetClass, calledName, 0, visited, cu));
|
||||||
targetClass, calledName, 0, visited, cu, budget));
|
|
||||||
} else {
|
} else {
|
||||||
String val = constantResolver.resolve(tracedRight, context);
|
String val = constantResolver.resolve(tracedRight, context);
|
||||||
if (val != null) results.add(val);
|
if (val != null) results.add(val);
|
||||||
@@ -117,8 +106,7 @@ public class ConstructorAnalyzer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||||
results.addAll(constantExtractor.resolveMethodReturnConstant(
|
results.addAll(constantExtractor.resolveMethodReturnConstant(targetClass, calledName, 0, visited, cu));
|
||||||
targetClass, calledName, 0, visited, cu, budget));
|
|
||||||
} else {
|
} else {
|
||||||
String val = constantResolver.resolve(tracedRight, context);
|
String val = constantResolver.resolve(tracedRight, context);
|
||||||
if (val != null) results.add(val);
|
if (val != null) results.add(val);
|
||||||
@@ -166,37 +154,35 @@ public class ConstructorAnalyzer {
|
|||||||
if (superFqn != null && !superFqn.equals("java.lang.Object")) {
|
if (superFqn != null && !superFqn.equals("java.lang.Object")) {
|
||||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||||
if (superTd != null) {
|
if (superTd != null) {
|
||||||
results.addAll(traceFieldInConstructors(superTd, fieldName, context, visited, budget.child()));
|
results.addAll(traceFieldInConstructors(superTd, fieldName, context, visited));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (results.isEmpty()) {
|
if (results.isEmpty()) {
|
||||||
// Context-Aware Fallback (Down): widen only for interface/abstract types
|
// Context-Aware Fallback (Down): Check subclasses/implementations
|
||||||
if (shouldWidenToImplementations(td)) {
|
List<String> impls = context.getImplementations(fqn);
|
||||||
List<String> impls = context.getImplementations(fqn);
|
if (impls != null) {
|
||||||
if (impls != null) {
|
for (String implFqn : impls) {
|
||||||
for (String implFqn : impls) {
|
TypeDeclaration implTd = context.getTypeDeclaration(implFqn);
|
||||||
TypeDeclaration implTd = context.getTypeDeclaration(implFqn);
|
if (implTd != null) {
|
||||||
if (implTd != null) {
|
results.addAll(traceFieldInConstructors(implTd, fieldName, context, visited));
|
||||||
results.addAll(traceFieldInConstructors(implTd, fieldName, context, visited, budget.child()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (results.isEmpty() && budget != null && budget.allowGlobalFieldScan()) {
|
if (results.isEmpty()) {
|
||||||
// Global Fallback: Search all parsed classes in the workspace (explicit opt-in only)
|
// Global Fallback: Search all parsed classes in the workspace
|
||||||
Collection<TypeDeclaration> allTypes = context.getTypeDeclarations();
|
Collection<TypeDeclaration> allTypes = context.getTypeDeclarations();
|
||||||
if (allTypes != null) {
|
if (allTypes != null) {
|
||||||
int matchedCount = 0;
|
int matchedCount = 0;
|
||||||
for (TypeDeclaration typeDecl : allTypes) {
|
for (TypeDeclaration typeDecl : allTypes) {
|
||||||
if (typeDecl == null) continue;
|
if (typeDecl == null) continue;
|
||||||
if (context.hasField(typeDecl, fieldName)) {
|
if (context.hasField(typeDecl, fieldName)) {
|
||||||
results.addAll(traceFieldInConstructors(typeDecl, fieldName, context, visited, budget.child()));
|
results.addAll(traceFieldInConstructors(typeDecl, fieldName, context, visited));
|
||||||
matchedCount++;
|
matchedCount++;
|
||||||
if (matchedCount >= budget.globalFieldScanLimit()) {
|
if (matchedCount >= 10) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,13 +194,6 @@ public class ConstructorAnalyzer {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean shouldWidenToImplementations(TypeDeclaration td) {
|
|
||||||
if (td == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return td.isInterface() || Modifier.isAbstract(td.getModifiers());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, String> buildFieldValuesFromCIC(
|
public Map<String, String> buildFieldValuesFromCIC(
|
||||||
TypeDeclaration td,
|
TypeDeclaration td,
|
||||||
ClassInstanceCreation cic,
|
ClassInstanceCreation cic,
|
||||||
@@ -486,7 +465,7 @@ public class ConstructorAnalyzer {
|
|||||||
private ClassInstanceCreation findReturnedCIC(
|
private ClassInstanceCreation findReturnedCIC(
|
||||||
String className, String methodName, CodebaseContext context,
|
String className, String methodName, CodebaseContext context,
|
||||||
Set<String> visited, int depth) {
|
Set<String> visited, int depth) {
|
||||||
if (ResolutionBudget.defaults().isReturnedCicExhausted(depth) || !visited.add(className + "#" + methodName)) {
|
if (depth > 50 || !visited.add(className + "#" + methodName)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,172 @@ package click.kamil.springstatemachineexporter.analysis.service;
|
|||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
|
||||||
* Placeholder kept for API stability. The exporter never executes Maven or Gradle against the
|
|
||||||
* analyzed project. Multi-module layout is discovered statically via
|
|
||||||
* {@link click.kamil.springstatemachineexporter.analysis.resolver.SiblingDependencyResolver}
|
|
||||||
* (parsed {@code settings.gradle}, {@code pom.xml}, {@code build.gradle}).
|
|
||||||
*/
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class DynamicClasspathResolver {
|
public class DynamicClasspathResolver {
|
||||||
|
|
||||||
public List<String> resolveClasspath(Path projectRoot) {
|
public List<String> resolveClasspath(Path projectRoot) {
|
||||||
log.debug("Skipping external JAR classpath: analysis uses scanned source files only.");
|
log.info("Attempting dynamic classpath resolution for project at {}", projectRoot);
|
||||||
|
if (projectRoot == null || !Files.exists(projectRoot)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.exists(projectRoot.resolve("pom.xml"))) {
|
||||||
|
return resolveMaven(projectRoot);
|
||||||
|
} else if (Files.exists(projectRoot.resolve("build.gradle")) || Files.exists(projectRoot.resolve("build.gradle.kts"))) {
|
||||||
|
return resolveGradle(projectRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("No Maven or Gradle configuration found at {}. Proceeding without dynamic classpath.", projectRoot);
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected List<String> resolveMaven(Path projectRoot) {
|
||||||
|
log.info("Maven project detected. Resolving dependencies...");
|
||||||
|
try {
|
||||||
|
String mvnCmd = getCommand(projectRoot, "mvnw", "mvn");
|
||||||
|
if (mvnCmd == null) {
|
||||||
|
log.warn("Maven executable not found.");
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(
|
||||||
|
mvnCmd,
|
||||||
|
"-q",
|
||||||
|
"dependency:build-classpath",
|
||||||
|
"-DincludeScope=compile",
|
||||||
|
"-Dmdep.outputFile=sme_cp.txt"
|
||||||
|
);
|
||||||
|
pb.directory(projectRoot.toFile());
|
||||||
|
|
||||||
|
runProcess(pb);
|
||||||
|
|
||||||
|
java.util.Set<String> allPaths = new java.util.LinkedHashSet<>();
|
||||||
|
try (java.util.stream.Stream<Path> stream = Files.walk(projectRoot)) {
|
||||||
|
stream.filter(p -> p.getFileName().toString().equals("sme_cp.txt"))
|
||||||
|
.forEach(cpFile -> {
|
||||||
|
try {
|
||||||
|
String cpString = Files.readString(cpFile).trim();
|
||||||
|
if (!cpString.isEmpty()) {
|
||||||
|
allPaths.addAll(Arrays.asList(cpString.split(File.pathSeparator)));
|
||||||
|
}
|
||||||
|
Files.deleteIfExists(cpFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to read or delete sme_cp.txt at {}", cpFile, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!allPaths.isEmpty()) {
|
||||||
|
return new ArrayList<>(allPaths);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to dynamically resolve Maven classpath", e);
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<String> resolveGradle(Path projectRoot) {
|
||||||
|
log.info("Gradle project detected. Resolving dependencies...");
|
||||||
|
Path initScript = null;
|
||||||
|
try {
|
||||||
|
initScript = Files.createTempFile("state_machine_exporter_init", ".gradle");
|
||||||
|
String gradleCmd = getCommand(projectRoot, "gradlew", "gradle");
|
||||||
|
if (gradleCmd == null) {
|
||||||
|
log.warn("Gradle executable not found.");
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
String scriptContent = """
|
||||||
|
allprojects {
|
||||||
|
task smePrintClasspath {
|
||||||
|
doLast {
|
||||||
|
def cp = []
|
||||||
|
if (configurations.findByName("compileClasspath")) {
|
||||||
|
cp = configurations.compileClasspath.files
|
||||||
|
}
|
||||||
|
println "SME_CLASSPATH_MARKER:" + cp.join(File.pathSeparator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Files.writeString(initScript, scriptContent);
|
||||||
|
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(
|
||||||
|
gradleCmd,
|
||||||
|
"-q",
|
||||||
|
"-I", initScript.toAbsolutePath().toString(),
|
||||||
|
"smePrintClasspath"
|
||||||
|
);
|
||||||
|
pb.directory(projectRoot.toFile());
|
||||||
|
|
||||||
|
String output = runProcessAndGetOutput(pb);
|
||||||
|
java.util.Set<String> allPaths = new java.util.LinkedHashSet<>();
|
||||||
|
for (String line : output.split("\\r?\\n")) {
|
||||||
|
if (line.startsWith("SME_CLASSPATH_MARKER:")) {
|
||||||
|
String cpString = line.substring("SME_CLASSPATH_MARKER:".length()).trim();
|
||||||
|
if (!cpString.isEmpty()) {
|
||||||
|
allPaths.addAll(Arrays.asList(cpString.split(File.pathSeparator)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!allPaths.isEmpty()) {
|
||||||
|
return new ArrayList<>(allPaths);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to dynamically resolve Gradle classpath", e);
|
||||||
|
} finally {
|
||||||
|
if (initScript != null) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(initScript);
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getCommand(Path projectRoot, String wrapper, String systemBinary) {
|
||||||
|
Path wrapperUnix = projectRoot.resolve(wrapper);
|
||||||
|
Path wrapperWin = projectRoot.resolve(wrapper + ".bat");
|
||||||
|
|
||||||
|
boolean isWin = System.getProperty("os.name").toLowerCase().contains("win");
|
||||||
|
|
||||||
|
if (isWin && Files.exists(wrapperWin)) return wrapperWin.toAbsolutePath().toString();
|
||||||
|
if (!isWin && Files.exists(wrapperUnix)) {
|
||||||
|
// Need absolute path like ./gradlew
|
||||||
|
return wrapperUnix.toAbsolutePath().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemBinary;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void runProcess(ProcessBuilder pb) throws IOException, InterruptedException {
|
||||||
|
pb.redirectErrorStream(true);
|
||||||
|
pb.redirectOutput(ProcessBuilder.Redirect.DISCARD);
|
||||||
|
Process p = pb.start();
|
||||||
|
p.waitFor();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String runProcessAndGetOutput(ProcessBuilder pb) throws IOException, InterruptedException {
|
||||||
|
pb.redirectErrorStream(true);
|
||||||
|
Process p = pb.start();
|
||||||
|
StringBuilder out = new StringBuilder();
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
out.append(line).append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.waitFor();
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +1,17 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.AnalysisEnricher;
|
import click.kamil.springstatemachineexporter.analysis.enricher.AnalysisEnricher;
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.CallChainEnricher;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.EntryPointEnricher;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.PropertyEnricher;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerCanonicalizationEnricher;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.path.ForwardPathEstimationEnricher;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class EnrichmentService {
|
public class EnrichmentService {
|
||||||
private final List<AnalysisEnricher> prePropertyEnrichers;
|
private final List<AnalysisEnricher> enrichers;
|
||||||
private final List<AnalysisEnricher> postPropertyEnrichers;
|
|
||||||
|
|
||||||
/** Single-phase pipeline (all enrichers run together; used by focused tests). */
|
|
||||||
public EnrichmentService(List<AnalysisEnricher> enrichers) {
|
|
||||||
this(enrichers, List.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
public EnrichmentService(
|
|
||||||
List<AnalysisEnricher> prePropertyEnrichers,
|
|
||||||
List<AnalysisEnricher> postPropertyEnrichers) {
|
|
||||||
this.prePropertyEnrichers = prePropertyEnrichers;
|
|
||||||
this.postPropertyEnrichers = postPropertyEnrichers;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static EnrichmentService createDefault() {
|
|
||||||
return new EnrichmentService(
|
|
||||||
List.of(
|
|
||||||
new TriggerEnricher(),
|
|
||||||
new EntryPointEnricher(),
|
|
||||||
new PropertyEnricher()),
|
|
||||||
List.of(
|
|
||||||
new CallChainEnricher(),
|
|
||||||
new ForwardPathEstimationEnricher(),
|
|
||||||
new TriggerCanonicalizationEnricher(),
|
|
||||||
new TransitionLinkerEnricher()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||||
enrichPreProperty(result, context, intelligence);
|
|
||||||
enrichPostProperty(result, context, intelligence);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void enrichPreProperty(
|
|
||||||
AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
|
||||||
runEnrichers(prePropertyEnrichers, result, context, intelligence);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void enrichPostProperty(
|
|
||||||
AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
|
||||||
runEnrichers(postPropertyEnrichers, result, context, intelligence);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Re-runs transition linking after property resolution without rebuilding call chains. */
|
|
||||||
public void relinkAfterPropertyResolution(
|
|
||||||
AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
|
||||||
runEnrichers(
|
|
||||||
List.of(new TriggerCanonicalizationEnricher(), new TransitionLinkerEnricher()),
|
|
||||||
result,
|
|
||||||
context,
|
|
||||||
intelligence);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void runEnrichers(
|
|
||||||
List<AnalysisEnricher> enrichers,
|
|
||||||
AnalysisResult result,
|
|
||||||
CodebaseContext context,
|
|
||||||
CodebaseIntelligenceProvider intelligence) {
|
|
||||||
for (AnalysisEnricher enricher : enrichers) {
|
for (AnalysisEnricher enricher : enrichers) {
|
||||||
enricher.enrich(result, context, intelligence);
|
enricher.enrich(result, context, intelligence);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,183 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.*;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expands REST entry points whose {@code @PathVariable} parameters flow into string-keyed
|
|
||||||
* switch mappers (e.g. {@code StringCommandMapper.fromString}) into concrete binding variants.
|
|
||||||
*/
|
|
||||||
public final class EntryPointBindingExpander {
|
|
||||||
|
|
||||||
private EntryPointBindingExpander() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<Map<String, String>> expandPathVariableBindings(
|
|
||||||
EntryPoint entryPoint,
|
|
||||||
CodebaseContext context,
|
|
||||||
Map<String, List<CallEdge>> callGraph) {
|
|
||||||
if (entryPoint == null || entryPoint.getParameters() == null || entryPoint.getParameters().isEmpty()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
|
|
||||||
if (!isPathOrQueryVariable(parameter)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
List<String> cases = resolveStringCasesForParameter(
|
|
||||||
entryPoint.getClassName() + "." + entryPoint.getMethodName(),
|
|
||||||
parameter.getName(),
|
|
||||||
context,
|
|
||||||
callGraph);
|
|
||||||
if (cases.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
List<Map<String, String>> variants = new ArrayList<>();
|
|
||||||
for (String caseValue : cases) {
|
|
||||||
variants.add(Map.of(parameter.getName(), caseValue));
|
|
||||||
}
|
|
||||||
return variants;
|
|
||||||
}
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static EntryPoint withResolvedPath(EntryPoint entryPoint, Map<String, String> bindings) {
|
|
||||||
if (entryPoint == null || bindings == null || bindings.isEmpty()) {
|
|
||||||
return entryPoint;
|
|
||||||
}
|
|
||||||
String name = entryPoint.getName();
|
|
||||||
Map<String, String> metadata = entryPoint.getMetadata() == null
|
|
||||||
? new LinkedHashMap<>()
|
|
||||||
: new LinkedHashMap<>(entryPoint.getMetadata());
|
|
||||||
for (Map.Entry<String, String> binding : bindings.entrySet()) {
|
|
||||||
String placeholder = "{" + binding.getKey() + "}";
|
|
||||||
if (name != null) {
|
|
||||||
name = name.replace(placeholder, binding.getValue());
|
|
||||||
}
|
|
||||||
String path = metadata.get("path");
|
|
||||||
if (path != null) {
|
|
||||||
metadata.put("path", path.replace(placeholder, binding.getValue()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return EntryPoint.builder()
|
|
||||||
.type(entryPoint.getType())
|
|
||||||
.name(name)
|
|
||||||
.className(entryPoint.getClassName())
|
|
||||||
.methodName(entryPoint.getMethodName())
|
|
||||||
.sourceFile(entryPoint.getSourceFile())
|
|
||||||
.metadata(metadata)
|
|
||||||
.parameters(entryPoint.getParameters())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isPathOrQueryVariable(EntryPoint.Parameter parameter) {
|
|
||||||
if (parameter.getAnnotations() == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return parameter.getAnnotations().stream()
|
|
||||||
.anyMatch(a -> "PathVariable".equals(a) || "RequestParam".equals(a));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<String> resolveStringCasesForParameter(
|
|
||||||
String startMethod,
|
|
||||||
String paramName,
|
|
||||||
CodebaseContext context,
|
|
||||||
Map<String, List<CallEdge>> callGraph) {
|
|
||||||
ArrayDeque<String> queue = new ArrayDeque<>();
|
|
||||||
Set<String> visited = new HashSet<>();
|
|
||||||
queue.add(startMethod);
|
|
||||||
while (!queue.isEmpty()) {
|
|
||||||
String methodFqn = queue.poll();
|
|
||||||
if (!visited.add(methodFqn)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (visited.size() > 12) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
List<String> localCases = extractStringSwitchCases(methodFqn, paramName, context);
|
|
||||||
if (!localCases.isEmpty()) {
|
|
||||||
return localCases;
|
|
||||||
}
|
|
||||||
List<CallEdge> edges = callGraph.get(methodFqn);
|
|
||||||
if (edges == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (CallEdge edge : edges) {
|
|
||||||
if (edge.getTargetMethod() != null) {
|
|
||||||
queue.add(edge.getTargetMethod());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<String> extractStringSwitchCases(String methodFqn, String paramName, CodebaseContext context) {
|
|
||||||
MethodDeclaration method = findMethodDeclaration(methodFqn, context);
|
|
||||||
if (method == null || method.getBody() == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
if (!hasParameter(method, paramName)) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
LinkedHashSet<String> cases = new LinkedHashSet<>();
|
|
||||||
method.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(SwitchStatement node) {
|
|
||||||
collectCasesFromSwitch(node.getExpression(), node.statements(), paramName, cases);
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean visit(SwitchExpression node) {
|
|
||||||
collectCasesFromSwitch(node.getExpression(), node.statements(), paramName, cases);
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return List.copyOf(cases);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void collectCasesFromSwitch(
|
|
||||||
Expression selector,
|
|
||||||
List<?> statements,
|
|
||||||
String paramName,
|
|
||||||
Set<String> cases) {
|
|
||||||
if (!(selector instanceof SimpleName simpleName) || !paramName.equals(simpleName.getIdentifier())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (Object statementObj : statements) {
|
|
||||||
if (statementObj instanceof SwitchCase switchCase && !switchCase.isDefault()) {
|
|
||||||
for (Object exprObj : switchCase.expressions()) {
|
|
||||||
if (exprObj instanceof StringLiteral stringLiteral) {
|
|
||||||
cases.add(stringLiteral.getLiteralValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean hasParameter(MethodDeclaration method, String paramName) {
|
|
||||||
for (Object paramObj : method.parameters()) {
|
|
||||||
if (paramObj instanceof SingleVariableDeclaration param
|
|
||||||
&& paramName.equals(param.getName().getIdentifier())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MethodDeclaration findMethodDeclaration(String methodFqn, CodebaseContext context) {
|
|
||||||
if (methodFqn == null || !methodFqn.contains(".")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
|
||||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
|
||||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(className);
|
|
||||||
if (typeDeclaration == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return context.findMethodDeclaration(typeDeclaration, methodName, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.*;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Classifies trailing expression access (map lookup vs traceable accessor) using AST structure and
|
|
||||||
* declared receiver types instead of string heuristics.
|
|
||||||
*/
|
|
||||||
public final class ExpressionAccessClassifier {
|
|
||||||
|
|
||||||
public enum AccessKind {
|
|
||||||
/** {@code Map.get(key)}, {@code list.get(i)} — keep intact for {@link ConstantExtractor}. */
|
|
||||||
KEYED_LOOKUP,
|
|
||||||
/** {@code getEvent()}, {@code supplier.get()} — split receiver/suffix for call-path tracing. */
|
|
||||||
TRACEABLE_ACCESSOR,
|
|
||||||
NONE
|
|
||||||
}
|
|
||||||
|
|
||||||
private final CodebaseContext context;
|
|
||||||
private final VariableTracer variableTracer;
|
|
||||||
private final ConstantResolver constantResolver;
|
|
||||||
private final Function<String, ASTNode> expressionParser;
|
|
||||||
|
|
||||||
public ExpressionAccessClassifier(
|
|
||||||
CodebaseContext context,
|
|
||||||
VariableTracer variableTracer,
|
|
||||||
ConstantResolver constantResolver,
|
|
||||||
Function<String, ASTNode> expressionParser) {
|
|
||||||
this.context = context;
|
|
||||||
this.variableTracer = variableTracer;
|
|
||||||
this.constantResolver = constantResolver;
|
|
||||||
this.expressionParser = expressionParser;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccessKind classifyTrailingAccess(String expression, String scopeMethod) {
|
|
||||||
if (expression == null || expression.isBlank()) {
|
|
||||||
return AccessKind.NONE;
|
|
||||||
}
|
|
||||||
ASTNode node = expressionParser.apply(expression);
|
|
||||||
while (node instanceof ParenthesizedExpression pe) {
|
|
||||||
node = pe.getExpression();
|
|
||||||
}
|
|
||||||
if (node instanceof ArrayAccess) {
|
|
||||||
return AccessKind.NONE;
|
|
||||||
}
|
|
||||||
if (node instanceof MethodInvocation mi) {
|
|
||||||
return classifyMethodInvocation(mi, scopeMethod);
|
|
||||||
}
|
|
||||||
return AccessKind.NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccessKind classifyMethodInvocation(MethodInvocation mi, String scopeMethod) {
|
|
||||||
if (mi == null) {
|
|
||||||
return AccessKind.NONE;
|
|
||||||
}
|
|
||||||
String methodName = mi.getName().getIdentifier();
|
|
||||||
int argCount = mi.arguments().size();
|
|
||||||
|
|
||||||
if ("getOrDefault".equals(methodName) && argCount >= 1) {
|
|
||||||
return AccessKind.KEYED_LOOKUP;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("get".equals(methodName)) {
|
|
||||||
String receiverType = resolveReceiverType(mi.getExpression(), scopeMethod);
|
|
||||||
if (argCount == 0) {
|
|
||||||
if (isMapLikeType(receiverType)) {
|
|
||||||
return AccessKind.KEYED_LOOKUP;
|
|
||||||
}
|
|
||||||
return AccessKind.TRACEABLE_ACCESSOR;
|
|
||||||
}
|
|
||||||
if (isMapLikeType(receiverType) || isIndexedCollectionType(receiverType)) {
|
|
||||||
return AccessKind.KEYED_LOOKUP;
|
|
||||||
}
|
|
||||||
if (receiverExpressionIsMapLiteral(mi.getExpression())) {
|
|
||||||
return AccessKind.KEYED_LOOKUP;
|
|
||||||
}
|
|
||||||
return AccessKind.TRACEABLE_ACCESSOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (argCount == 0 && (isBeanAccessorName(methodName) || isRecordStyleAccessor(methodName))) {
|
|
||||||
return AccessKind.TRACEABLE_ACCESSOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
return AccessKind.TRACEABLE_ACCESSOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isKeyedLookup(String expression, String scopeMethod) {
|
|
||||||
return classifyTrailingAccess(expression, scopeMethod) == AccessKind.KEYED_LOOKUP;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isKeyedLookup(MethodInvocation mi, String scopeMethod) {
|
|
||||||
return classifyMethodInvocation(mi, scopeMethod) == AccessKind.KEYED_LOOKUP;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether a trailing suffix like {@code .getEvent()} or {@code .type()} should be resolved via accessor tracing.
|
|
||||||
*/
|
|
||||||
public boolean isTraceableAccessorSuffix(String suffix) {
|
|
||||||
if (suffix == null || !suffix.startsWith(".")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String methodPart = suffix.substring(1);
|
|
||||||
if (methodPart.endsWith("()")) {
|
|
||||||
methodPart = methodPart.substring(0, methodPart.length() - 2);
|
|
||||||
}
|
|
||||||
return isBeanAccessorName(methodPart) || isRecordStyleAccessor(methodPart);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isTraceableAccessorExpression(String expression, String scopeMethod) {
|
|
||||||
if (expression == null || expression.isBlank()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
AccessKind kind = classifyTrailingAccess(expression, scopeMethod);
|
|
||||||
return kind == AccessKind.TRACEABLE_ACCESSOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean receiverExpressionIsMapLiteral(Expression receiver) {
|
|
||||||
if (constantResolver == null || context == null || receiver == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String resolved = constantResolver.resolve(receiver, context);
|
|
||||||
return resolved != null && resolved.startsWith("MAP:");
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveReceiverType(Expression receiver, String scopeMethod) {
|
|
||||||
if (receiver == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (receiverExpressionIsMapLiteral(receiver)) {
|
|
||||||
return "Map";
|
|
||||||
}
|
|
||||||
if (receiver instanceof SimpleName sn) {
|
|
||||||
return lookupDeclaredType(scopeMethod, sn.getIdentifier());
|
|
||||||
}
|
|
||||||
if (receiver instanceof ThisExpression) {
|
|
||||||
return classNameFromScopeMethod(scopeMethod);
|
|
||||||
}
|
|
||||||
if (receiver instanceof FieldAccess fa) {
|
|
||||||
String fieldName = fa.getName().getIdentifier();
|
|
||||||
if (fa.getExpression() instanceof ThisExpression) {
|
|
||||||
return lookupFieldType(classNameFromScopeMethod(scopeMethod), fieldName);
|
|
||||||
}
|
|
||||||
if (fa.getExpression() instanceof SimpleName outer) {
|
|
||||||
String outerType = lookupDeclaredType(scopeMethod, outer.getIdentifier());
|
|
||||||
return lookupFieldType(rawTypeName(outerType), fieldName);
|
|
||||||
}
|
|
||||||
if (fa.getExpression() instanceof QualifiedName qn) {
|
|
||||||
String outerType = resolveQualifiedTypeName(qn);
|
|
||||||
return lookupFieldType(outerType, fieldName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (receiver instanceof QualifiedName qn) {
|
|
||||||
String typeName = resolveQualifiedTypeName(qn);
|
|
||||||
if (typeName != null) {
|
|
||||||
String fieldName = qn.getName().getIdentifier();
|
|
||||||
String fieldType = lookupFieldType(typeName, fieldName);
|
|
||||||
if (fieldType != null) {
|
|
||||||
return fieldType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (receiver instanceof MethodInvocation chainMi) {
|
|
||||||
AccessKind kind = classifyMethodInvocation(chainMi, scopeMethod);
|
|
||||||
if (kind == AccessKind.TRACEABLE_ACCESSOR) {
|
|
||||||
String getterName = chainMi.getName().getIdentifier();
|
|
||||||
String baseType = resolveReceiverType(chainMi.getExpression(), scopeMethod);
|
|
||||||
return lookupMethodReturnType(rawTypeName(baseType), getterName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String lookupDeclaredType(String scopeMethod, String name) {
|
|
||||||
if (variableTracer == null || scopeMethod == null || name == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return variableTracer.getVariableDeclaredType(scopeMethod, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String lookupFieldType(String typeName, String fieldName) {
|
|
||||||
if (context == null || typeName == null || fieldName == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(typeName);
|
|
||||||
if (td == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return findFieldType(td, fieldName, new HashSet<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
private String findFieldType(TypeDeclaration td, String fieldName, Set<String> visited) {
|
|
||||||
String typeFqn = context.getFqn(td);
|
|
||||||
if (typeFqn == null || !visited.add(typeFqn)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (FieldDeclaration fd : td.getFields()) {
|
|
||||||
for (Object fragObj : fd.fragments()) {
|
|
||||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
|
||||||
if (frag.getName().getIdentifier().equals(fieldName)) {
|
|
||||||
return fd.getType().toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String superFqn = context.getSuperclassFqn(td);
|
|
||||||
if (superFqn != null) {
|
|
||||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
|
||||||
if (superTd != null) {
|
|
||||||
String inherited = findFieldType(superTd, fieldName, visited);
|
|
||||||
if (inherited != null) {
|
|
||||||
return inherited;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String lookupMethodReturnType(String typeName, String methodName) {
|
|
||||||
if (context == null || typeName == null || methodName == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(typeName);
|
|
||||||
if (td == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
|
||||||
if (md != null && md.getReturnType2() != null) {
|
|
||||||
return md.getReturnType2().toString();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveQualifiedTypeName(QualifiedName qn) {
|
|
||||||
String full = qn.getFullyQualifiedName();
|
|
||||||
if (full.contains(".")) {
|
|
||||||
return full.substring(0, full.lastIndexOf('.'));
|
|
||||||
}
|
|
||||||
return classNameFromScopeMethod(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String classNameFromScopeMethod(String scopeMethod) {
|
|
||||||
if (scopeMethod == null || !scopeMethod.contains(".")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return scopeMethod.substring(0, scopeMethod.lastIndexOf('.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isBeanAccessorName(String methodName) {
|
|
||||||
return (methodName.startsWith("get") && methodName.length() > 3)
|
|
||||||
|| (methodName.startsWith("is") && methodName.length() > 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isRecordStyleAccessor(String methodName) {
|
|
||||||
return "type".equals(methodName) || "event".equals(methodName) || "name".equals(methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isMapLikeType(String type) {
|
|
||||||
String raw = rawTypeName(type);
|
|
||||||
if (raw == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return "Map".equals(raw) || raw.endsWith("Map");
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isIndexedCollectionType(String type) {
|
|
||||||
String raw = rawTypeName(type);
|
|
||||||
if (raw == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return "List".equals(raw) || raw.endsWith("List")
|
|
||||||
|| "Set".equals(raw) || raw.endsWith("Set")
|
|
||||||
|| raw.endsWith("Deque") || "Queue".equals(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String rawTypeName(String type) {
|
|
||||||
if (type == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String raw = type.trim();
|
|
||||||
if (raw.contains("<")) {
|
|
||||||
raw = raw.substring(0, raw.indexOf('<'));
|
|
||||||
}
|
|
||||||
if (raw.contains(".")) {
|
|
||||||
raw = raw.substring(raw.lastIndexOf('.') + 1);
|
|
||||||
}
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,6 @@ import org.eclipse.jdt.core.dom.*;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -20,20 +19,6 @@ public class GenericEventDetector {
|
|||||||
private final ConstantResolver constantResolver;
|
private final ConstantResolver constantResolver;
|
||||||
private final List<LibraryHint> hints;
|
private final List<LibraryHint> hints;
|
||||||
|
|
||||||
/** Parameter names that route to a machine, not SM state enum constants. */
|
|
||||||
private static final Set<String> ROUTING_PARAMETER_NAMES = Set.of(
|
|
||||||
"machineType",
|
|
||||||
"machineId",
|
|
||||||
"machineName",
|
|
||||||
"domain",
|
|
||||||
"type",
|
|
||||||
"action",
|
|
||||||
"eventString",
|
|
||||||
"version");
|
|
||||||
|
|
||||||
private static final Set<String> TRIGGER_METHOD_NAMES = Set.of(
|
|
||||||
"sendEvent", "sendEvents", "sendEventCollect", "sendEventMono", "fire", "trigger");
|
|
||||||
|
|
||||||
public List<TriggerPoint> detect(CompilationUnit cu) {
|
public List<TriggerPoint> detect(CompilationUnit cu) {
|
||||||
List<TriggerPoint> triggers = new ArrayList<>();
|
List<TriggerPoint> triggers = new ArrayList<>();
|
||||||
String fileName = "unknown";
|
String fileName = "unknown";
|
||||||
@@ -48,13 +33,15 @@ public class GenericEventDetector {
|
|||||||
public boolean visit(MethodInvocation node) {
|
public boolean visit(MethodInvocation node) {
|
||||||
String methodName = node.getName().getIdentifier();
|
String methodName = node.getName().getIdentifier();
|
||||||
|
|
||||||
if (TRIGGER_METHOD_NAMES.contains(methodName)) {
|
if ("sendEvent".equals(methodName) || "sendEvents".equals(methodName) ||
|
||||||
|
"sendEventCollect".equals(methodName) || "sendEventMono".equals(methodName)) {
|
||||||
processSendEvent(node, cu, triggers);
|
processSendEvent(node, cu, triggers);
|
||||||
} else {
|
} else {
|
||||||
for (Object argObj : node.arguments()) {
|
for (Object argObj : node.arguments()) {
|
||||||
if (argObj instanceof ExpressionMethodReference emr) {
|
if (argObj instanceof ExpressionMethodReference emr) {
|
||||||
String refMethod = emr.getName().getIdentifier();
|
String refMethod = emr.getName().getIdentifier();
|
||||||
if (TRIGGER_METHOD_NAMES.contains(refMethod)) {
|
if ("sendEvent".equals(refMethod) || "sendEvents".equals(refMethod) ||
|
||||||
|
"sendEventCollect".equals(refMethod) || "sendEventMono".equals(refMethod)) {
|
||||||
processSendEventMethodReference(node, emr, cu, triggers);
|
processSendEventMethodReference(node, emr, cu, triggers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,7 +64,7 @@ public class GenericEventDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MethodDeclaration method = findEnclosingMethod(node);
|
MethodDeclaration method = findEnclosingMethod(node);
|
||||||
AbstractTypeDeclaration type = findEnclosingAbstractType(node);
|
TypeDeclaration type = findEnclosingType(node);
|
||||||
if (type == null) return;
|
if (type == null) return;
|
||||||
|
|
||||||
String sourceState = extractSourceState(node);
|
String sourceState = extractSourceState(node);
|
||||||
@@ -204,11 +191,18 @@ public class GenericEventDetector {
|
|||||||
|
|
||||||
if (receiver instanceof SimpleName sn) {
|
if (receiver instanceof SimpleName sn) {
|
||||||
String name = sn.getIdentifier();
|
String name = sn.getIdentifier();
|
||||||
AbstractTypeDeclaration enclosing = findEnclosingAbstractType(node);
|
// Check if it's a field in the enclosing class
|
||||||
if (enclosing != null) {
|
TypeDeclaration td = findEnclosingType(node);
|
||||||
Type memberType = lookupMemberType(enclosing, name);
|
if (td != null) {
|
||||||
if (memberType != null) {
|
for (FieldDeclaration fd : td.getFields()) {
|
||||||
return memberType + "." + methodName;
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
if (fragObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
if (fragment.getName().getIdentifier().equals(name)) {
|
||||||
|
String typeName = fd.getType().toString();
|
||||||
|
return typeName + "." + methodName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return name + "." + methodName;
|
return name + "." + methodName;
|
||||||
@@ -236,7 +230,7 @@ public class GenericEventDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MethodDeclaration method = findEnclosingMethod(node);
|
MethodDeclaration method = findEnclosingMethod(node);
|
||||||
AbstractTypeDeclaration type = findEnclosingAbstractType(node);
|
TypeDeclaration type = findEnclosingType(node);
|
||||||
|
|
||||||
if (type == null) return Collections.emptyList();
|
if (type == null) return Collections.emptyList();
|
||||||
|
|
||||||
@@ -309,10 +303,8 @@ public class GenericEventDetector {
|
|||||||
String state = extractStateFromSiblings(current, block.statements());
|
String state = extractStateFromSiblings(current, block.statements());
|
||||||
if (state != null) return state;
|
if (state != null) return state;
|
||||||
} else if (parent instanceof SwitchStatement switchStmt) {
|
} else if (parent instanceof SwitchStatement switchStmt) {
|
||||||
if (!isRoutingParameter(switchStmt.getExpression())) {
|
String state = extractStateFromSiblings(current, switchStmt.statements());
|
||||||
String state = extractStateFromSiblings(current, switchStmt.statements());
|
if (state != null) return state;
|
||||||
if (state != null) return state;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
current = parent;
|
current = parent;
|
||||||
@@ -369,10 +361,6 @@ public class GenericEventDetector {
|
|||||||
if (left instanceof NullLiteral || right instanceof NullLiteral) {
|
if (left instanceof NullLiteral || right instanceof NullLiteral) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRoutingParameter(left) || isRoutingParameter(right)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usually one is a method call like getState() or a variable like `state`
|
// Usually one is a method call like getState() or a variable like `state`
|
||||||
// and the other is the constant enum like `OrderState.PENDING` or `"PENDING"`
|
// and the other is the constant enum like `OrderState.PENDING` or `"PENDING"`
|
||||||
@@ -393,10 +381,6 @@ public class GenericEventDetector {
|
|||||||
if (("equals".equals(methodName) || "equalsIgnoreCase".equals(methodName)) && !mi.arguments().isEmpty()) {
|
if (("equals".equals(methodName) || "equalsIgnoreCase".equals(methodName)) && !mi.arguments().isEmpty()) {
|
||||||
Expression receiver = mi.getExpression();
|
Expression receiver = mi.getExpression();
|
||||||
Expression arg = (Expression) mi.arguments().get(0);
|
Expression arg = (Expression) mi.arguments().get(0);
|
||||||
|
|
||||||
if (isRoutingParameter(receiver) || isRoutingParameter(arg)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If receiver is null (e.g., implicit this), fall back to arg
|
// If receiver is null (e.g., implicit this), fall back to arg
|
||||||
if (receiver == null) {
|
if (receiver == null) {
|
||||||
@@ -418,13 +402,6 @@ public class GenericEventDetector {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isRoutingParameter(Expression expr) {
|
|
||||||
if (expr instanceof SimpleName sn) {
|
|
||||||
return ROUTING_PARAMETER_NAMES.contains(sn.getIdentifier());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getSimpleNameString(Expression expr) {
|
private String getSimpleNameString(Expression expr) {
|
||||||
if (expr instanceof QualifiedName qn) {
|
if (expr instanceof QualifiedName qn) {
|
||||||
return qn.getName().getIdentifier();
|
return qn.getName().getIdentifier();
|
||||||
@@ -539,49 +516,11 @@ public class GenericEventDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||||
AbstractTypeDeclaration type = findEnclosingAbstractType(node);
|
|
||||||
return type instanceof TypeDeclaration td ? td : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private AbstractTypeDeclaration findEnclosingAbstractType(ASTNode node) {
|
|
||||||
ASTNode parent = node.getParent();
|
ASTNode parent = node.getParent();
|
||||||
while (parent != null && !(parent instanceof AbstractTypeDeclaration)) {
|
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||||
parent = parent.getParent();
|
parent = parent.getParent();
|
||||||
}
|
}
|
||||||
return (AbstractTypeDeclaration) parent;
|
return (TypeDeclaration) parent;
|
||||||
}
|
|
||||||
|
|
||||||
private List<FieldDeclaration> getFieldDeclarations(AbstractTypeDeclaration type) {
|
|
||||||
if (type instanceof TypeDeclaration td) {
|
|
||||||
return List.of(td.getFields());
|
|
||||||
}
|
|
||||||
if (type instanceof RecordDeclaration rd) {
|
|
||||||
return List.of(rd.getFields());
|
|
||||||
}
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Type lookupMemberType(AbstractTypeDeclaration enclosingType, String memberName) {
|
|
||||||
if (enclosingType == null || memberName == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (FieldDeclaration fd : getFieldDeclarations(enclosingType)) {
|
|
||||||
for (Object fragObj : fd.fragments()) {
|
|
||||||
if (fragObj instanceof VariableDeclarationFragment fragment
|
|
||||||
&& fragment.getName().getIdentifier().equals(memberName)) {
|
|
||||||
return fd.getType();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (enclosingType instanceof RecordDeclaration rd) {
|
|
||||||
for (Object compObj : rd.recordComponents()) {
|
|
||||||
if (compObj instanceof SingleVariableDeclaration svd
|
|
||||||
&& svd.getName().getIdentifier().equals(memberName)) {
|
|
||||||
return svd.getType();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String[] resolveStateMachineTypeArguments(MethodInvocation node) {
|
private String[] resolveStateMachineTypeArguments(MethodInvocation node) {
|
||||||
@@ -637,7 +576,7 @@ public class GenericEventDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String resolveGenericTypeVariable(String typeVarName, ASTNode node, CompilationUnit cu) {
|
private String resolveGenericTypeVariable(String typeVarName, ASTNode node, CompilationUnit cu) {
|
||||||
AbstractTypeDeclaration enclosingClass = findEnclosingAbstractType(node);
|
TypeDeclaration enclosingClass = findEnclosingType(node);
|
||||||
if (enclosingClass == null) return typeVarName;
|
if (enclosingClass == null) return typeVarName;
|
||||||
String className = context.getFqn(enclosingClass);
|
String className = context.getFqn(enclosingClass);
|
||||||
if (className == null) return typeVarName;
|
if (className == null) return typeVarName;
|
||||||
@@ -654,11 +593,7 @@ public class GenericEventDetector {
|
|||||||
String baseName = resolveTypeToFqn(baseType, (CompilationUnit) implNode.getRoot());
|
String baseName = resolveTypeToFqn(baseType, (CompilationUnit) implNode.getRoot());
|
||||||
if (className.equals(baseName) || className.endsWith("." + baseName)) {
|
if (className.equals(baseName) || className.endsWith("." + baseName)) {
|
||||||
int typeIndex = -1;
|
int typeIndex = -1;
|
||||||
List<?> typeParams = enclosingClass instanceof TypeDeclaration typeDecl
|
List<?> typeParams = enclosingClass.typeParameters();
|
||||||
? typeDecl.typeParameters()
|
|
||||||
: enclosingClass instanceof RecordDeclaration recordDecl
|
|
||||||
? recordDecl.typeParameters()
|
|
||||||
: List.of();
|
|
||||||
for (int i = 0; i < typeParams.size(); i++) {
|
for (int i = 0; i < typeParams.size(); i++) {
|
||||||
TypeParameter tp = (TypeParameter) typeParams.get(i);
|
TypeParameter tp = (TypeParameter) typeParams.get(i);
|
||||||
if (tp.getName().getIdentifier().equals(typeVarName)) {
|
if (tp.getName().getIdentifier().equals(typeVarName)) {
|
||||||
@@ -730,12 +665,17 @@ public class GenericEventDetector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check fields and record components
|
// Check fields
|
||||||
AbstractTypeDeclaration enclosingType = findEnclosingAbstractType(node);
|
TypeDeclaration enclosingType = findEnclosingType(node);
|
||||||
if (enclosingType != null) {
|
if (enclosingType != null) {
|
||||||
Type memberType = lookupMemberType(enclosingType, varName);
|
for (FieldDeclaration fd : enclosingType.getFields()) {
|
||||||
if (memberType != null) {
|
for (Object fragObj : fd.fragments()) {
|
||||||
return memberType;
|
if (fragObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
if (fragment.getName().getIdentifier().equals(varName)) {
|
||||||
|
return fd.getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
this.injectionAnalyzer = injectionAnalyzer;
|
this.injectionAnalyzer = injectionAnalyzer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, List<CallEdge>> getCallGraph() {
|
||||||
|
return buildCallGraph();
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
protected Map<String, List<CallEdge>> buildCallGraph() {
|
protected Map<String, List<CallEdge>> buildCallGraph() {
|
||||||
Map<String, List<CallEdge>> cached = (Map<String, List<CallEdge>>) context.getCache().get("jdtCallGraph");
|
Map<String, List<CallEdge>> cached = (Map<String, List<CallEdge>>) context.getCache().get("jdtCallGraph");
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import java.util.Map;
|
|||||||
public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
||||||
private final CodebaseContext context;
|
private final CodebaseContext context;
|
||||||
private final Path rootDir;
|
private final Path rootDir;
|
||||||
private final ConstantResolver constantResolver;
|
private final ConstantResolver constantResolver = new ConstantResolver();
|
||||||
private final GenericEventDetector eventDetector;
|
private final GenericEventDetector eventDetector;
|
||||||
private final SpringMvcDetector mvcDetector;
|
private final SpringMvcDetector mvcDetector;
|
||||||
private final MessagingDetector messagingDetector;
|
private final MessagingDetector messagingDetector;
|
||||||
@@ -31,12 +31,10 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
|||||||
private final SpringComponentDetector componentDetector;
|
private final SpringComponentDetector componentDetector;
|
||||||
private List<TriggerPoint> cachedTriggers;
|
private List<TriggerPoint> cachedTriggers;
|
||||||
private List<EntryPoint> cachedEntryPoints;
|
private List<EntryPoint> cachedEntryPoints;
|
||||||
private List<CallChain> cachedCallChains;
|
|
||||||
|
|
||||||
public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) {
|
public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.rootDir = rootDir;
|
this.rootDir = rootDir;
|
||||||
this.constantResolver = context.getConstantResolver();
|
|
||||||
this.eventDetector = new GenericEventDetector(context, constantResolver, context.getLibraryHints());
|
this.eventDetector = new GenericEventDetector(context, constantResolver, context.getLibraryHints());
|
||||||
this.mvcDetector = new SpringMvcDetector(context, constantResolver);
|
this.mvcDetector = new SpringMvcDetector(context, constantResolver);
|
||||||
this.messagingDetector = new MessagingDetector(context);
|
this.messagingDetector = new MessagingDetector(context);
|
||||||
@@ -94,17 +92,8 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
public List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||||
if (cachedCallChains != null) {
|
log.info("JdtIntelligenceProvider searching for call chains between {} entry points and {} triggers", entryPoints.size(), triggers.size());
|
||||||
return cachedCallChains;
|
return callGraphEngine.findChains(entryPoints, triggers);
|
||||||
}
|
|
||||||
List<EntryPoint> allEntryPoints = findEntryPoints();
|
|
||||||
List<TriggerPoint> allTriggers = triggers != null && !triggers.isEmpty()
|
|
||||||
? triggers
|
|
||||||
: findTriggerPoints();
|
|
||||||
log.info("JdtIntelligenceProvider searching for call chains between {} entry points and {} triggers",
|
|
||||||
allEntryPoints.size(), allTriggers.size());
|
|
||||||
cachedCallChains = callGraphEngine.findChains(allEntryPoints, allTriggers);
|
|
||||||
return cachedCallChains;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/** Builds a minimal {@link CodebaseContext} for JSON re-export when sources are available. */
|
|
||||||
@Slf4j
|
|
||||||
public final class JsonExportContextFactory {
|
|
||||||
|
|
||||||
private JsonExportContextFactory() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Path findProjectRoot(Path start) {
|
|
||||||
Path current = start != null ? start.toAbsolutePath().normalize() : null;
|
|
||||||
while (current != null) {
|
|
||||||
if (Files.exists(current.resolve("settings.gradle"))
|
|
||||||
|| Files.exists(current.resolve("pom.xml"))
|
|
||||||
|| Files.exists(current.resolve("build.gradle"))) {
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
current = current.getParent();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CodebaseContext scanProjectRoot(Path projectRoot, List<String> activeProfiles) throws IOException {
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
if (activeProfiles != null) {
|
|
||||||
context.setActiveProfiles(activeProfiles);
|
|
||||||
}
|
|
||||||
context.setProjectRoot(projectRoot);
|
|
||||||
context.setResolveBindings(true);
|
|
||||||
|
|
||||||
Path mainJava = projectRoot.resolve("src/main/java");
|
|
||||||
if (Files.isDirectory(mainJava)) {
|
|
||||||
context.setSourcepath(List.of(mainJava.toString()));
|
|
||||||
context.scan(projectRoot);
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
click.kamil.springstatemachineexporter.analysis.resolver.SiblingDependencyResolver siblingResolver =
|
|
||||||
new click.kamil.springstatemachineexporter.analysis.resolver.SiblingDependencyResolver();
|
|
||||||
var projectGraph = siblingResolver.analyzeProject(projectRoot);
|
|
||||||
var scanPaths = projectGraph.resolveScanPaths(projectRoot, false);
|
|
||||||
context.scan(scanPaths, java.util.Collections.emptySet());
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StateMachineTypeResolver.MachineTypes resolveMachineTypes(
|
|
||||||
String machineName,
|
|
||||||
String embeddedStateTypeFqn,
|
|
||||||
String embeddedEventTypeFqn,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (context != null && machineName != null) {
|
|
||||||
StateMachineTypeResolver.MachineTypes resolved =
|
|
||||||
StateMachineTypeResolver.resolveTypes(machineName, context);
|
|
||||||
if (resolved.stateTypeFqn() != null || resolved.eventTypeFqn() != null) {
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new StateMachineTypeResolver.MachineTypes(embeddedStateTypeFqn, embeddedEventTypeFqn);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean hasPackageQualifiedType(String typeFqn) {
|
|
||||||
return typeFqn != null && typeFqn.contains(".");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -40,12 +40,6 @@ public class MessagingDetector {
|
|||||||
} else if (typeName.endsWith("RabbitListener")) {
|
} else if (typeName.endsWith("RabbitListener")) {
|
||||||
type = EntryPoint.Type.RABBIT;
|
type = EntryPoint.Type.RABBIT;
|
||||||
destination = resolveValue(annotation, "queues");
|
destination = resolveValue(annotation, "queues");
|
||||||
if ("unknown".equals(destination)) {
|
|
||||||
destination = resolveValue(annotation, "queue");
|
|
||||||
}
|
|
||||||
if ("unknown".equals(destination)) {
|
|
||||||
destination = resolveValue(annotation, "bindings");
|
|
||||||
}
|
|
||||||
} else if (typeName.endsWith("SqsListener")) {
|
} else if (typeName.endsWith("SqsListener")) {
|
||||||
type = EntryPoint.Type.SQS;
|
type = EntryPoint.Type.SQS;
|
||||||
destination = resolveValue(annotation, "value");
|
destination = resolveValue(annotation, "value");
|
||||||
@@ -61,9 +55,6 @@ public class MessagingDetector {
|
|||||||
} else if (typeName.endsWith("KafkaListener")) {
|
} else if (typeName.endsWith("KafkaListener")) {
|
||||||
type = EntryPoint.Type.KAFKA;
|
type = EntryPoint.Type.KAFKA;
|
||||||
destination = resolveValue(annotation, "topics");
|
destination = resolveValue(annotation, "topics");
|
||||||
if ("unknown".equals(destination)) {
|
|
||||||
destination = resolveValue(annotation, "topicPattern");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type != null) {
|
if (type != null) {
|
||||||
|
|||||||
@@ -1,365 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.*;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Walks a call path forward, propagating known parameter / local bindings so switch-case
|
|
||||||
* edge constraints can prune impossible branches (e.g. {@code dispatch} arms).
|
|
||||||
*/
|
|
||||||
public class PathBindingEvaluator {
|
|
||||||
|
|
||||||
private final CodebaseContext context;
|
|
||||||
private final VariableTracer variableTracer;
|
|
||||||
private final ConstantResolver constantResolver;
|
|
||||||
private final TypeResolver typeResolver;
|
|
||||||
|
|
||||||
public PathBindingEvaluator(
|
|
||||||
CodebaseContext context,
|
|
||||||
VariableTracer variableTracer,
|
|
||||||
ConstantResolver constantResolver,
|
|
||||||
TypeResolver typeResolver) {
|
|
||||||
this.context = context;
|
|
||||||
this.variableTracer = variableTracer;
|
|
||||||
this.constantResolver = constantResolver;
|
|
||||||
this.typeResolver = typeResolver;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Visible for tests: bindings accumulated while walking a path forward (ignores constraint rejection). */
|
|
||||||
Map<String, String> traceBindings(List<String> path, Map<String, List<CallEdge>> callGraph, CallGraphPathFinder pathFinder) {
|
|
||||||
Map<String, String> bindings = new HashMap<>();
|
|
||||||
if (path == null || path.size() < 2) {
|
|
||||||
return bindings;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < path.size() - 1; i++) {
|
|
||||||
String caller = path.get(i);
|
|
||||||
String target = path.get(i + 1);
|
|
||||||
CallEdge edge = findEdge(caller, target, callGraph, pathFinder);
|
|
||||||
if (edge == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
enrichBindings(caller, target, edge, path, i + 1, callGraph, pathFinder, bindings);
|
|
||||||
}
|
|
||||||
return bindings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isPathCompatible(
|
|
||||||
List<String> path,
|
|
||||||
Map<String, List<CallEdge>> callGraph,
|
|
||||||
CallGraphPathFinder pathFinder,
|
|
||||||
Map<String, String> initialBindings) {
|
|
||||||
if (path == null || path.size() < 2) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
Map<String, String> bindings = new HashMap<>();
|
|
||||||
if (initialBindings != null) {
|
|
||||||
bindings.putAll(initialBindings);
|
|
||||||
}
|
|
||||||
for (int i = 0; i < path.size() - 1; i++) {
|
|
||||||
String caller = path.get(i);
|
|
||||||
String target = path.get(i + 1);
|
|
||||||
CallEdge edge = findEdge(caller, target, callGraph, pathFinder);
|
|
||||||
if (edge == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (edge.getConstraint() != null
|
|
||||||
&& !edge.getConstraint().isBlank()
|
|
||||||
&& !BooleanConstraintEvaluator.isCompatibleWithBindings(edge.getConstraint(), bindings)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
enrichBindings(caller, target, edge, path, i + 1, callGraph, pathFinder, bindings);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isPathCompatible(List<String> path, Map<String, List<CallEdge>> callGraph, CallGraphPathFinder pathFinder) {
|
|
||||||
return isPathCompatible(path, callGraph, pathFinder, Map.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns {@code true} when {@code caller}→{@code callee} via {@code edge} satisfies the edge constraint
|
|
||||||
* under {@code bindings}; on success, {@code bindings} is updated with propagated parameter values.
|
|
||||||
*/
|
|
||||||
public boolean tryTraverseEdge(
|
|
||||||
String caller,
|
|
||||||
String callee,
|
|
||||||
CallEdge edge,
|
|
||||||
List<String> pathWithCallee,
|
|
||||||
int calleeIndex,
|
|
||||||
Map<String, List<CallEdge>> callGraph,
|
|
||||||
CallGraphPathFinder pathFinder,
|
|
||||||
Map<String, String> bindings) {
|
|
||||||
if (edge.getConstraint() != null
|
|
||||||
&& !edge.getConstraint().isBlank()
|
|
||||||
&& !BooleanConstraintEvaluator.isCompatibleWithBindings(edge.getConstraint(), bindings)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (variableTracer == null || typeResolver == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
enrichBindings(caller, callee, edge, pathWithCallee, calleeIndex, callGraph, pathFinder, bindings);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void enrichBindings(
|
|
||||||
String caller,
|
|
||||||
String target,
|
|
||||||
CallEdge edge,
|
|
||||||
List<String> path,
|
|
||||||
int pathIndex,
|
|
||||||
Map<String, List<CallEdge>> callGraph,
|
|
||||||
CallGraphPathFinder pathFinder,
|
|
||||||
Map<String, String> bindings) {
|
|
||||||
Map<String, String> paramValues = variableTracer.buildParameterValuesMap(caller, target, callGraph, path, pathIndex);
|
|
||||||
mergeResolvedBindings(bindings, paramValues, caller);
|
|
||||||
|
|
||||||
List<String> targetParams = typeResolver.getParameterNames(target);
|
|
||||||
if (targetParams == null || edge.getArguments() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < edge.getArguments().size() && i < targetParams.size(); i++) {
|
|
||||||
String arg = edge.getArguments().get(i);
|
|
||||||
String paramName = targetParams.get(i);
|
|
||||||
String resolved = resolveBindingValue(caller, arg, bindings);
|
|
||||||
if (shouldStoreBinding(paramName, resolved)) {
|
|
||||||
bindings.put(paramName, resolved);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldStoreBinding(String paramName, String resolved) {
|
|
||||||
if (resolved == null || resolved.isBlank() || resolved.equals(paramName)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (resolved.contains("(") && !isEnumLikeConstant(resolved)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void mergeResolvedBindings(Map<String, String> bindings, Map<String, String> paramValues, String caller) {
|
|
||||||
if (paramValues == null || paramValues.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (Map.Entry<String, String> entry : paramValues.entrySet()) {
|
|
||||||
String resolved = resolveBindingValue(caller, entry.getValue(), bindings);
|
|
||||||
if (shouldStoreBinding(entry.getKey(), resolved)) {
|
|
||||||
bindings.put(entry.getKey(), resolved);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveBindingValue(String callerFqn, String rawValue, Map<String, String> bindings) {
|
|
||||||
if (rawValue == null || rawValue.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (rawValue.startsWith("\"") || isEnumLikeConstant(rawValue)) {
|
|
||||||
return normalizeLiteral(rawValue);
|
|
||||||
}
|
|
||||||
if (bindings.containsKey(rawValue)) {
|
|
||||||
return bindings.get(rawValue);
|
|
||||||
}
|
|
||||||
String traced = variableTracer.traceLocalVariable(callerFqn, rawValue, bindings);
|
|
||||||
if (traced != null && !traced.isBlank()) {
|
|
||||||
if (traced.startsWith("\"") || isEnumLikeConstant(traced)) {
|
|
||||||
return traced;
|
|
||||||
}
|
|
||||||
String fromCall = resolveMethodCallFromSource(callerFqn, rawValue, bindings);
|
|
||||||
if (fromCall != null) {
|
|
||||||
return fromCall;
|
|
||||||
}
|
|
||||||
if (!traced.equals(rawValue)) {
|
|
||||||
return traced;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rawValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveMethodCallFromSource(String callerFqn, String varName, Map<String, String> bindings) {
|
|
||||||
MethodDeclaration md = findMethodDeclaration(callerFqn);
|
|
||||||
if (md == null || md.getBody() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final Expression[] initializer = new Expression[1];
|
|
||||||
md.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(VariableDeclarationFragment node) {
|
|
||||||
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
|
||||||
initializer[0] = node.getInitializer();
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (initializer[0] == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return evaluateExpressionValue(initializer[0], callerFqn, bindings);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String evaluateExpressionValue(Expression expr, String callerFqn, Map<String, String> bindings) {
|
|
||||||
if (expr instanceof StringLiteral sl) {
|
|
||||||
return "\"" + sl.getLiteralValue() + "\"";
|
|
||||||
}
|
|
||||||
if (expr instanceof QualifiedName qn) {
|
|
||||||
return qn.getFullyQualifiedName();
|
|
||||||
}
|
|
||||||
if (expr instanceof SimpleName sn) {
|
|
||||||
return bindings.getOrDefault(sn.getIdentifier(), sn.getIdentifier());
|
|
||||||
}
|
|
||||||
if (expr instanceof MethodInvocation mi) {
|
|
||||||
return evaluateMethodInvocationReturn(mi, callerFqn, bindings);
|
|
||||||
}
|
|
||||||
if (expr instanceof SwitchExpression se) {
|
|
||||||
return constantResolver.evaluateSwitchWithParams(se, bindings, context);
|
|
||||||
}
|
|
||||||
String resolved = constantResolver.resolve(expr, context);
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String evaluateMethodInvocationReturn(MethodInvocation mi, String callerFqn, Map<String, String> bindings) {
|
|
||||||
String ownerFqn = resolveMethodOwnerFqn(mi, callerFqn);
|
|
||||||
if (ownerFqn == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String methodName = mi.getName().getIdentifier();
|
|
||||||
TypeDeclaration owner = context.getTypeDeclaration(ownerFqn);
|
|
||||||
if (owner == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
MethodDeclaration md = context.findMethodDeclaration(owner, methodName, true);
|
|
||||||
if (md == null || md.getBody() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, String> callBindings = new HashMap<>(bindings);
|
|
||||||
for (int i = 0; i < md.parameters().size() && i < mi.arguments().size(); i++) {
|
|
||||||
SingleVariableDeclaration param = (SingleVariableDeclaration) md.parameters().get(i);
|
|
||||||
String paramName = param.getName().getIdentifier();
|
|
||||||
String argValue = evaluateExpressionValue((Expression) mi.arguments().get(i), callerFqn, callBindings);
|
|
||||||
if (argValue != null) {
|
|
||||||
callBindings.put(paramName, argValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final String[] result = new String[1];
|
|
||||||
md.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(ReturnStatement node) {
|
|
||||||
if (node.getExpression() instanceof SwitchExpression se) {
|
|
||||||
result[0] = constantResolver.evaluateSwitchWithParams(se, callBindings, context);
|
|
||||||
} else if (node.getExpression() != null) {
|
|
||||||
result[0] = evaluateExpressionValue(node.getExpression(), ownerFqn + "." + methodName, callBindings);
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveFieldTypeFqn(String callerFqn, String fieldName) {
|
|
||||||
if (callerFqn == null || !callerFqn.contains(".")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String className = callerFqn.substring(0, callerFqn.lastIndexOf('.'));
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
|
||||||
if (td == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (FieldDeclaration field : td.getFields()) {
|
|
||||||
for (Object fragmentObj : field.fragments()) {
|
|
||||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) fragmentObj;
|
|
||||||
if (fragment.getName().getIdentifier().equals(fieldName)) {
|
|
||||||
IVariableBinding binding = fragment.resolveBinding();
|
|
||||||
if (binding != null && binding.getType() != null) {
|
|
||||||
return binding.getType().getErasure().getQualifiedName();
|
|
||||||
}
|
|
||||||
String simpleType = field.getType().toString();
|
|
||||||
TypeDeclaration resolved = context.getTypeDeclaration(simpleType);
|
|
||||||
if (resolved != null) {
|
|
||||||
return context.getFqn(resolved);
|
|
||||||
}
|
|
||||||
if (td.getRoot() instanceof CompilationUnit cu && cu.getPackage() != null) {
|
|
||||||
String pkg = cu.getPackage().getName().getFullyQualifiedName();
|
|
||||||
resolved = context.getTypeDeclaration(pkg + "." + simpleType);
|
|
||||||
if (resolved != null) {
|
|
||||||
return context.getFqn(resolved);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return simpleType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveMethodOwnerFqn(MethodInvocation mi, String callerFqn) {
|
|
||||||
IMethodBinding methodBinding = mi.resolveMethodBinding();
|
|
||||||
if (methodBinding != null && methodBinding.getDeclaringClass() != null) {
|
|
||||||
return methodBinding.getDeclaringClass().getErasure().getQualifiedName();
|
|
||||||
}
|
|
||||||
Expression receiver = mi.getExpression();
|
|
||||||
if (receiver instanceof SimpleName sn) {
|
|
||||||
return resolveFieldTypeFqn(callerFqn, sn.getIdentifier());
|
|
||||||
}
|
|
||||||
if (receiver instanceof FieldAccess fa) {
|
|
||||||
IVariableBinding fieldBinding = fa.resolveFieldBinding();
|
|
||||||
if (fieldBinding != null && fieldBinding.getType() != null) {
|
|
||||||
return fieldBinding.getType().getErasure().getQualifiedName();
|
|
||||||
}
|
|
||||||
return resolveFieldTypeFqn(callerFqn, fa.getName().getIdentifier());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MethodDeclaration findMethodDeclaration(String methodFqn) {
|
|
||||||
if (methodFqn == null || !methodFqn.contains(".")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
|
||||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
|
||||||
if (td == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return context.findMethodDeclaration(td, methodName, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String normalizeLiteral(String value) {
|
|
||||||
if (value != null && value.startsWith("\"") && value.endsWith("\"")) {
|
|
||||||
return value.substring(1, value.length() - 1);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isEnumLikeConstant(String value) {
|
|
||||||
if (value == null || value.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
int dot = value.lastIndexOf('.');
|
|
||||||
if (dot <= 0 || dot >= value.length() - 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return Character.isUpperCase(value.charAt(dot + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CallEdge findEdge(
|
|
||||||
String caller, String target, Map<String, List<CallEdge>> callGraph, CallGraphPathFinder pathFinder) {
|
|
||||||
List<CallEdge> edges = callGraph.get(caller);
|
|
||||||
if (edges == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (CallEdge edge : edges) {
|
|
||||||
if (edge.getTargetMethod().equals(target) || pathFinder.isHeuristicMatch(edge.getTargetMethod(), target)) {
|
|
||||||
return edge;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -182,11 +182,11 @@ public class SpringMvcDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String getHttpVerb(String annotationName) {
|
private String getHttpVerb(String annotationName) {
|
||||||
if (annotationName.endsWith("PostMapping") || annotationName.endsWith("POST") || "POST".equals(annotationName)) return "POST";
|
if (annotationName.endsWith("PostMapping") || annotationName.endsWith("POST")) return "POST";
|
||||||
if (annotationName.endsWith("GetMapping") || annotationName.endsWith("GET") || "GET".equals(annotationName)) return "GET";
|
if (annotationName.endsWith("GetMapping") || annotationName.endsWith("GET")) return "GET";
|
||||||
if (annotationName.endsWith("PutMapping") || annotationName.endsWith("PUT") || "PUT".equals(annotationName)) return "PUT";
|
if (annotationName.endsWith("PutMapping") || annotationName.endsWith("PUT")) return "PUT";
|
||||||
if (annotationName.endsWith("DeleteMapping") || annotationName.endsWith("DELETE") || "DELETE".equals(annotationName)) return "DELETE";
|
if (annotationName.endsWith("DeleteMapping") || annotationName.endsWith("DELETE")) return "DELETE";
|
||||||
if (annotationName.endsWith("PatchMapping") || annotationName.endsWith("PATCH") || "PATCH".equals(annotationName)) return "PATCH";
|
if (annotationName.endsWith("PatchMapping") || annotationName.endsWith("PATCH")) return "PATCH";
|
||||||
if (annotationName.endsWith("RequestMapping")) return "REQUEST";
|
if (annotationName.endsWith("RequestMapping")) return "REQUEST";
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Classpath entries from the exporter tool itself — not from running Maven/Gradle on the analyzed project.
|
|
||||||
*/
|
|
||||||
public final class ToolingClasspath {
|
|
||||||
|
|
||||||
private ToolingClasspath() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<String> currentJvmJarEntries() {
|
|
||||||
String raw = System.getProperty("java.class.path");
|
|
||||||
if (raw == null || raw.isBlank()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
return Arrays.stream(raw.split(File.pathSeparator))
|
|
||||||
.filter(entry -> entry.endsWith(".jar"))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -30,28 +30,6 @@ public class TypeResolver {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getParameterNames(String methodFqn) {
|
|
||||||
if (methodFqn == null || !methodFqn.contains(".")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
|
||||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
|
||||||
if (td == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
|
||||||
if (md == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
List<String> paramNames = new ArrayList<>();
|
|
||||||
for (Object paramObj : md.parameters()) {
|
|
||||||
SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
|
|
||||||
paramNames.add(param.getName().getIdentifier());
|
|
||||||
}
|
|
||||||
return paramNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getParameterName(String methodFqn, int index) {
|
public String getParameterName(String methodFqn, int index) {
|
||||||
if (methodFqn == null || !methodFqn.contains(".") || index < 0) return null;
|
if (methodFqn == null || !methodFqn.contains(".") || index < 0) return null;
|
||||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.MethodInvocationUnwrapper;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.TrivialAccessorDetector;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import org.eclipse.jdt.core.dom.*;
|
import org.eclipse.jdt.core.dom.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -59,47 +55,11 @@ public class VariableTracer {
|
|||||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
if (md != null && md.getBody() != null) {
|
if (md != null && md.getBody() != null) {
|
||||||
final Expression[] setterArg = new Expression[1];
|
final Expression[] setterArg = new Expression[1];
|
||||||
String propName;
|
String propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
|
||||||
String setterMethodName;
|
|
||||||
String indexedFieldName;
|
|
||||||
|
|
||||||
if (isBeanStyleAccessorName(getterName)) {
|
|
||||||
propName = propertyNameFromAccessor(getterName);
|
|
||||||
setterMethodName = "set" + capitalizeProperty(propName);
|
|
||||||
indexedFieldName = propName;
|
|
||||||
|
|
||||||
String varTypeName = getVariableDeclaredType(methodFqn, varName);
|
|
||||||
if (varTypeName != null) {
|
|
||||||
CompilationUnit contextCu = md.getRoot() instanceof CompilationUnit cu ? cu : null;
|
|
||||||
TypeDeclaration varType = contextCu != null
|
|
||||||
? context.getTypeDeclaration(varTypeName, contextCu)
|
|
||||||
: null;
|
|
||||||
if (varType == null) {
|
|
||||||
varType = context.getTypeDeclaration(varTypeName);
|
|
||||||
}
|
|
||||||
if (varType != null) {
|
|
||||||
Optional<AccessorSummary> indexedSetter = context.getAccessorIndex()
|
|
||||||
.lookup(context.getFqn(varType), setterMethodName);
|
|
||||||
if (indexedSetter.isPresent() && indexedSetter.get().isSetter()) {
|
|
||||||
setterMethodName = indexedSetter.get().methodName();
|
|
||||||
indexedFieldName = indexedSetter.get().fieldName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
|
|
||||||
setterMethodName = "set" + propName;
|
|
||||||
indexedFieldName = propName;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String resolvedSetterName = setterMethodName;
|
|
||||||
final String resolvedFieldName = indexedFieldName;
|
|
||||||
md.getBody().accept(new ASTVisitor() {
|
md.getBody().accept(new ASTVisitor() {
|
||||||
@Override
|
@Override
|
||||||
public boolean visit(MethodInvocation node) {
|
public boolean visit(MethodInvocation node) {
|
||||||
if (node.getName().getIdentifier().equals(resolvedSetterName)
|
if (node.getName().getIdentifier().equalsIgnoreCase("set" + propName) || node.getName().getIdentifier().equalsIgnoreCase(propName)) {
|
||||||
|| node.getName().getIdentifier().equalsIgnoreCase("set" + resolvedFieldName)
|
|
||||||
|| node.getName().getIdentifier().equalsIgnoreCase(resolvedFieldName)) {
|
|
||||||
if (node.getExpression() instanceof SimpleName sn && sn.getIdentifier().equals(varName)) {
|
if (node.getExpression() instanceof SimpleName sn && sn.getIdentifier().equals(varName)) {
|
||||||
if (!node.arguments().isEmpty()) {
|
if (!node.arguments().isEmpty()) {
|
||||||
setterArg[0] = (Expression) node.arguments().get(0);
|
setterArg[0] = (Expression) node.arguments().get(0);
|
||||||
@@ -112,13 +72,13 @@ public class VariableTracer {
|
|||||||
public boolean visit(Assignment node) {
|
public boolean visit(Assignment node) {
|
||||||
if (node.getLeftHandSide() instanceof FieldAccess fa) {
|
if (node.getLeftHandSide() instanceof FieldAccess fa) {
|
||||||
if (fa.getExpression() instanceof SimpleName faSn && faSn.getIdentifier().equals(varName)) {
|
if (fa.getExpression() instanceof SimpleName faSn && faSn.getIdentifier().equals(varName)) {
|
||||||
if (fa.getName().getIdentifier().equalsIgnoreCase(resolvedFieldName)) {
|
if (fa.getName().getIdentifier().equalsIgnoreCase(propName)) {
|
||||||
setterArg[0] = node.getRightHandSide();
|
setterArg[0] = node.getRightHandSide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (node.getLeftHandSide() instanceof QualifiedName qqn) {
|
} else if (node.getLeftHandSide() instanceof QualifiedName qqn) {
|
||||||
if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) {
|
if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) {
|
||||||
if (qqn.getName().getIdentifier().equalsIgnoreCase(resolvedFieldName)) {
|
if (qqn.getName().getIdentifier().equalsIgnoreCase(propName)) {
|
||||||
setterArg[0] = node.getRightHandSide();
|
setterArg[0] = node.getRightHandSide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,32 +92,6 @@ public class VariableTracer {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isBeanStyleAccessorName(String accessorName) {
|
|
||||||
return (accessorName.startsWith("get") && accessorName.length() > 3)
|
|
||||||
|| (accessorName.startsWith("is") && accessorName.length() > 2)
|
|
||||||
|| (accessorName.startsWith("set") && accessorName.length() > 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String propertyNameFromAccessor(String getterName) {
|
|
||||||
if (getterName.startsWith("get") && getterName.length() > 3) {
|
|
||||||
return TrivialAccessorDetector.decapitalize(getterName.substring(3));
|
|
||||||
}
|
|
||||||
if (getterName.startsWith("is") && getterName.length() > 2) {
|
|
||||||
return TrivialAccessorDetector.decapitalize(getterName.substring(2));
|
|
||||||
}
|
|
||||||
return getterName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String capitalizeProperty(String propertyName) {
|
|
||||||
if (propertyName == null || propertyName.isEmpty()) {
|
|
||||||
return propertyName;
|
|
||||||
}
|
|
||||||
if (propertyName.length() > 1 && Character.isUpperCase(propertyName.charAt(1))) {
|
|
||||||
return propertyName;
|
|
||||||
}
|
|
||||||
return Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getFieldType(TypeDeclaration td, String fieldName, CodebaseContext context, java.util.Set<String> visited) {
|
private String getFieldType(TypeDeclaration td, String fieldName, CodebaseContext context, java.util.Set<String> visited) {
|
||||||
if (td == null) return null;
|
if (td == null) return null;
|
||||||
String typeFqn = context.getFqn(td);
|
String typeFqn = context.getFqn(td);
|
||||||
@@ -722,7 +656,62 @@ public class VariableTracer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Expression unwrapMethodInvocation(MethodInvocation mi, int depth, String methodFqn) {
|
private Expression unwrapMethodInvocation(MethodInvocation mi, int depth, String methodFqn) {
|
||||||
return MethodInvocationUnwrapper.unwrap(mi, depth, methodFqn, context, miArg -> resolveTargetMethodFqn(miArg, methodFqn));
|
if (depth > 5) return mi;
|
||||||
|
|
||||||
|
String targetMethodFqn = resolveTargetMethodFqn(mi, methodFqn);
|
||||||
|
if (targetMethodFqn != null && targetMethodFqn.contains(".")) {
|
||||||
|
String className = targetMethodFqn.substring(0, targetMethodFqn.lastIndexOf('.'));
|
||||||
|
String methodName = targetMethodFqn.substring(targetMethodFqn.lastIndexOf('.') + 1);
|
||||||
|
|
||||||
|
TypeDeclaration currentTd = methodFqn != null && methodFqn.contains(".") ? context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.'))) : null;
|
||||||
|
CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className, cu);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration localMd = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (localMd != null) {
|
||||||
|
int paramIdx = getReturnedParameterIndex(localMd);
|
||||||
|
if (paramIdx >= 0 && paramIdx < mi.arguments().size()) {
|
||||||
|
Expression arg = peelExpression((Expression) mi.arguments().get(paramIdx));
|
||||||
|
if (arg instanceof MethodInvocation innerMi) {
|
||||||
|
return unwrapMethodInvocation(innerMi, depth + 1, methodFqn);
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mi.getExpression() != null) {
|
||||||
|
String exprStr = mi.getExpression().toString();
|
||||||
|
if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects") && !exprStr.equals("Mono") && !exprStr.equals("Flux")) {
|
||||||
|
return mi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mi.arguments().isEmpty()) {
|
||||||
|
String mName = mi.getName().getIdentifier();
|
||||||
|
String lowerName = mName.toLowerCase();
|
||||||
|
|
||||||
|
if (lowerName.startsWith("map") || lowerName.startsWith("parse") ||
|
||||||
|
lowerName.startsWith("convert") || lowerName.startsWith("to") ||
|
||||||
|
lowerName.startsWith("resolve") || lowerName.startsWith("build") ||
|
||||||
|
lowerName.startsWith("create") || lowerName.startsWith("from") ||
|
||||||
|
lowerName.startsWith("transform") || lowerName.startsWith("translate") ||
|
||||||
|
lowerName.startsWith("derive") || lowerName.startsWith("determine") ||
|
||||||
|
lowerName.startsWith("calculate") || lowerName.startsWith("decode") ||
|
||||||
|
lowerName.startsWith("extract") || lowerName.startsWith("get") ||
|
||||||
|
lowerName.startsWith("find") || lowerName.startsWith("validate")) {
|
||||||
|
return mi;
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression arg = peelExpression((Expression) mi.arguments().get(0));
|
||||||
|
if (arg instanceof MethodInvocation innerMi) {
|
||||||
|
return unwrapMethodInvocation(innerMi, depth + 1, methodFqn);
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
return mi;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveTargetMethodFqn(MethodInvocation mi, String currentMethodFqn) {
|
private String resolveTargetMethodFqn(MethodInvocation mi, String currentMethodFqn) {
|
||||||
@@ -744,4 +733,33 @@ public class VariableTracer {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int getReturnedParameterIndex(MethodDeclaration md) {
|
||||||
|
if (md.getBody() == null) return -1;
|
||||||
|
List<?> parameters = md.parameters();
|
||||||
|
if (parameters.isEmpty()) return -1;
|
||||||
|
|
||||||
|
final List<String> returnedExprs = new ArrayList<>();
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(ReturnStatement node) {
|
||||||
|
if (node.getExpression() != null) {
|
||||||
|
returnedExprs.add(node.getExpression().toString());
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (returnedExprs.size() == 1) {
|
||||||
|
String retStr = returnedExprs.get(0);
|
||||||
|
for (int i = 0; i < parameters.size(); i++) {
|
||||||
|
if (parameters.get(i) instanceof SingleVariableDeclaration svd) {
|
||||||
|
if (svd.getName().getIdentifier().equals(retStr)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.validation;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Thrown when {@link AnalysisCanonicalFormValidator} finds enum identifiers that are not
|
|
||||||
* package-canonical for the machine's resolved {@code <State, Event>} types.
|
|
||||||
*/
|
|
||||||
public class AnalysisCanonicalFormException extends IllegalStateException {
|
|
||||||
|
|
||||||
private final List<AnalysisCanonicalFormValidator.Violation> violations;
|
|
||||||
|
|
||||||
public AnalysisCanonicalFormException(List<AnalysisCanonicalFormValidator.Violation> violations) {
|
|
||||||
super(formatMessage(violations));
|
|
||||||
this.violations = List.copyOf(violations);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<AnalysisCanonicalFormValidator.Violation> getViolations() {
|
|
||||||
return violations;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String formatMessage(List<AnalysisCanonicalFormValidator.Violation> violations) {
|
|
||||||
StringBuilder sb = new StringBuilder("Analysis model contains non-canonical enum identifiers:");
|
|
||||||
for (AnalysisCanonicalFormValidator.Violation violation : violations) {
|
|
||||||
sb.append("\n - ").append(violation.path())
|
|
||||||
.append(": '").append(violation.actual())
|
|
||||||
.append("' (expected '").append(violation.expected()).append("')");
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.validation;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Event;
|
|
||||||
import click.kamil.springstatemachineexporter.model.State;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates that enum-backed identifiers across the analysis model share one canonical shape:
|
|
||||||
* {@code {package.EnumType.CONSTANT}} derived from the machine config's {@code <State, Event>} types.
|
|
||||||
*
|
|
||||||
* <p>String/primitive machines ({@code StateMachineConfigurerAdapter<String, String>}) are skipped.
|
|
||||||
*/
|
|
||||||
public final class AnalysisCanonicalFormValidator {
|
|
||||||
|
|
||||||
public record Violation(String path, String actual, String expected) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private AnalysisCanonicalFormValidator() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<Violation> validate(AnalysisResult result, CodebaseContext context) {
|
|
||||||
List<Violation> violations = new ArrayList<>();
|
|
||||||
if (result == null || context == null) {
|
|
||||||
return violations;
|
|
||||||
}
|
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes =
|
|
||||||
StateMachineTypeResolver.resolveTypes(result.getName(), context);
|
|
||||||
if (!hasPackageQualifiedEnumType(machineTypes.stateTypeFqn())
|
|
||||||
&& !hasPackageQualifiedEnumType(machineTypes.eventTypeFqn())) {
|
|
||||||
if (!StateMachineTypeResolver.extendsEnumStateMachineConfigurer(result.getName(), context)) {
|
|
||||||
return violations;
|
|
||||||
}
|
|
||||||
violations.add(new Violation(
|
|
||||||
"machineTypes",
|
|
||||||
describeTypes(machineTypes),
|
|
||||||
"package-qualified state/event enum types"));
|
|
||||||
return violations;
|
|
||||||
}
|
|
||||||
|
|
||||||
validateFields(result, machineTypes, violations);
|
|
||||||
return violations;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<Violation> validateWithMachineTypes(
|
|
||||||
AnalysisResult result,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
|
||||||
List<Violation> violations = new ArrayList<>();
|
|
||||||
if (result == null || machineTypes == null) {
|
|
||||||
return violations;
|
|
||||||
}
|
|
||||||
if (!hasPackageQualifiedEnumType(machineTypes.stateTypeFqn())
|
|
||||||
&& !hasPackageQualifiedEnumType(machineTypes.eventTypeFqn())) {
|
|
||||||
return violations;
|
|
||||||
}
|
|
||||||
|
|
||||||
validateFields(result, machineTypes, violations);
|
|
||||||
return violations;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void enforce(AnalysisResult result, CodebaseContext context) {
|
|
||||||
List<Violation> violations = validate(result, context);
|
|
||||||
if (!violations.isEmpty()) {
|
|
||||||
throw new AnalysisCanonicalFormException(violations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void enforceWithMachineTypes(
|
|
||||||
AnalysisResult result,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
|
||||||
List<Violation> violations = validateWithMachineTypes(result, machineTypes);
|
|
||||||
if (!violations.isEmpty()) {
|
|
||||||
throw new AnalysisCanonicalFormException(violations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void validateFields(
|
|
||||||
AnalysisResult result,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
|
||||||
List<Violation> violations) {
|
|
||||||
validateTransitions(result.getTransitions(), machineTypes, violations);
|
|
||||||
validateStateCollection(result.getStates(), machineTypes.stateTypeFqn(), "states", violations);
|
|
||||||
validateStateLabels(result.getStartStates(), machineTypes.stateTypeFqn(), "startStates", violations);
|
|
||||||
validateStateLabels(result.getEndStates(), machineTypes.stateTypeFqn(), "endStates", violations);
|
|
||||||
|
|
||||||
if (result.getMetadata() != null) {
|
|
||||||
validateTriggers(result.getMetadata().getTriggers(), machineTypes, violations);
|
|
||||||
validateCallChains(result.getMetadata().getCallChains(), machineTypes, violations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String describeTypes(StateMachineTypeResolver.MachineTypes machineTypes) {
|
|
||||||
return "stateTypeFqn=" + machineTypes.stateTypeFqn() + ", eventTypeFqn=" + machineTypes.eventTypeFqn();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean hasPackageQualifiedEnumType(String typeFqn) {
|
|
||||||
return typeFqn != null && typeFqn.contains(".") && !MachineEnumCanonicalizer.isStringOrPrimitiveType(typeFqn);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void validateTransitions(
|
|
||||||
List<Transition> transitions,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
|
||||||
List<Violation> violations) {
|
|
||||||
if (transitions == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < transitions.size(); i++) {
|
|
||||||
Transition transition = transitions.get(i);
|
|
||||||
String prefix = "transitions[" + i + "]";
|
|
||||||
if (transition.getEvent() != null) {
|
|
||||||
requireCanonical(
|
|
||||||
prefix + ".event.fullIdentifier",
|
|
||||||
transition.getEvent().fullIdentifier(),
|
|
||||||
machineTypes.eventTypeFqn(),
|
|
||||||
violations);
|
|
||||||
}
|
|
||||||
validateStateList(transition.getSourceStates(), machineTypes.stateTypeFqn(), prefix + ".sourceStates", violations);
|
|
||||||
validateStateList(transition.getTargetStates(), machineTypes.stateTypeFqn(), prefix + ".targetStates", violations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void validateStateList(
|
|
||||||
List<State> states,
|
|
||||||
String stateTypeFqn,
|
|
||||||
String pathPrefix,
|
|
||||||
List<Violation> violations) {
|
|
||||||
if (states == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < states.size(); i++) {
|
|
||||||
requireCanonical(pathPrefix + "[" + i + "].fullIdentifier", states.get(i).fullIdentifier(), stateTypeFqn, violations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void validateStateCollection(
|
|
||||||
Set<State> states,
|
|
||||||
String stateTypeFqn,
|
|
||||||
String pathPrefix,
|
|
||||||
List<Violation> violations) {
|
|
||||||
if (states == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int i = 0;
|
|
||||||
for (State state : states) {
|
|
||||||
requireCanonical(pathPrefix + "[" + i++ + "].fullIdentifier", state.fullIdentifier(), stateTypeFqn, violations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void validateStateLabels(
|
|
||||||
Set<String> labels,
|
|
||||||
String stateTypeFqn,
|
|
||||||
String pathPrefix,
|
|
||||||
List<Violation> violations) {
|
|
||||||
if (labels == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int i = 0;
|
|
||||||
for (String label : labels) {
|
|
||||||
requireCanonical(pathPrefix + "[" + i++ + "]", label, stateTypeFqn, violations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void validateTriggers(
|
|
||||||
List<TriggerPoint> triggers,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
|
||||||
List<Violation> violations) {
|
|
||||||
if (triggers == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < triggers.size(); i++) {
|
|
||||||
TriggerPoint trigger = triggers.get(i);
|
|
||||||
String prefix = "metadata.triggers[" + i + "]";
|
|
||||||
String eventTypeFqn = preferTypeFqn(trigger.getEventTypeFqn(), machineTypes.eventTypeFqn());
|
|
||||||
String stateTypeFqn = preferTypeFqn(trigger.getStateTypeFqn(), machineTypes.stateTypeFqn());
|
|
||||||
|
|
||||||
requireCanonical(prefix + ".event", trigger.getEvent(), eventTypeFqn, violations);
|
|
||||||
requireCanonical(prefix + ".sourceState", trigger.getSourceState(), stateTypeFqn, violations);
|
|
||||||
|
|
||||||
if (trigger.getPolymorphicEvents() != null) {
|
|
||||||
for (int j = 0; j < trigger.getPolymorphicEvents().size(); j++) {
|
|
||||||
requireCanonical(
|
|
||||||
prefix + ".polymorphicEvents[" + j + "]",
|
|
||||||
trigger.getPolymorphicEvents().get(j),
|
|
||||||
eventTypeFqn,
|
|
||||||
violations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void validateCallChains(
|
|
||||||
List<CallChain> callChains,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
|
||||||
List<Violation> violations) {
|
|
||||||
if (callChains == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < callChains.size(); i++) {
|
|
||||||
CallChain chain = callChains.get(i);
|
|
||||||
String prefix = "metadata.callChains[" + i + "]";
|
|
||||||
TriggerPoint trigger = chain.getTriggerPoint();
|
|
||||||
if (trigger != null) {
|
|
||||||
String triggerPrefix = prefix + ".triggerPoint";
|
|
||||||
String eventTypeFqn = preferTypeFqn(trigger.getEventTypeFqn(), machineTypes.eventTypeFqn());
|
|
||||||
String stateTypeFqn = preferTypeFqn(trigger.getStateTypeFqn(), machineTypes.stateTypeFqn());
|
|
||||||
requireCanonical(triggerPrefix + ".event", trigger.getEvent(), eventTypeFqn, violations);
|
|
||||||
requireCanonical(triggerPrefix + ".sourceState", trigger.getSourceState(), stateTypeFqn, violations);
|
|
||||||
if (trigger.getPolymorphicEvents() != null) {
|
|
||||||
for (int j = 0; j < trigger.getPolymorphicEvents().size(); j++) {
|
|
||||||
requireCanonical(
|
|
||||||
triggerPrefix + ".polymorphicEvents[" + j + "]",
|
|
||||||
trigger.getPolymorphicEvents().get(j),
|
|
||||||
eventTypeFqn,
|
|
||||||
violations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (chain.getMatchedTransitions() != null) {
|
|
||||||
for (int j = 0; j < chain.getMatchedTransitions().size(); j++) {
|
|
||||||
MatchedTransition matched = chain.getMatchedTransitions().get(j);
|
|
||||||
String matchedPrefix = prefix + ".matchedTransitions[" + j + "]";
|
|
||||||
requireCanonical(matchedPrefix + ".event", matched.getEvent(), machineTypes.eventTypeFqn(), violations);
|
|
||||||
requireCanonical(matchedPrefix + ".sourceState", matched.getSourceState(), machineTypes.stateTypeFqn(), violations);
|
|
||||||
requireCanonical(matchedPrefix + ".targetState", matched.getTargetState(), machineTypes.stateTypeFqn(), violations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String preferTypeFqn(String primary, String fallback) {
|
|
||||||
if (hasPackageQualifiedEnumType(primary)) {
|
|
||||||
return primary;
|
|
||||||
}
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void requireCanonical(
|
|
||||||
String path,
|
|
||||||
String value,
|
|
||||||
String enumTypeFqn,
|
|
||||||
List<Violation> violations) {
|
|
||||||
if (!MachineEnumCanonicalizer.isMachineEnumReference(value, enumTypeFqn)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String expected = MachineEnumCanonicalizer.canonicalizeLabel(value, enumTypeFqn);
|
|
||||||
if (!expected.equals(value)) {
|
|
||||||
violations.add(new Violation(path, value, expected));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -45,44 +45,20 @@ public class TransitionStateUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Set<State> findAllStates(Collection<Transition> transitions, Set<String> initialStates, Set<String> endStates) {
|
public static Set<State> findAllStates(Collection<Transition> transitions, Set<String> initialStates, Set<String> endStates) {
|
||||||
java.util.Map<String, State> byFullIdentifier = new java.util.LinkedHashMap<>();
|
Set<State> allStates = new java.util.HashSet<>();
|
||||||
if (transitions != null) {
|
if (transitions != null) {
|
||||||
transitions.forEach(t -> {
|
transitions.forEach(t -> {
|
||||||
mergeState(byFullIdentifier, t.getSourceStates());
|
if (t.getSourceStates() != null) allStates.addAll(t.getSourceStates());
|
||||||
mergeState(byFullIdentifier, t.getTargetStates());
|
if (t.getTargetStates() != null) allStates.addAll(t.getTargetStates());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (initialStates != null) {
|
if (initialStates != null) {
|
||||||
initialStates.forEach(label -> mergeState(byFullIdentifier, State.of(label)));
|
initialStates.forEach(s -> allStates.add(State.of(s)));
|
||||||
}
|
}
|
||||||
if (endStates != null) {
|
if (endStates != null) {
|
||||||
endStates.forEach(label -> mergeState(byFullIdentifier, State.of(label)));
|
endStates.forEach(s -> allStates.add(State.of(s)));
|
||||||
}
|
}
|
||||||
return new java.util.LinkedHashSet<>(byFullIdentifier.values());
|
return allStates;
|
||||||
}
|
|
||||||
|
|
||||||
private static void mergeState(java.util.Map<String, State> map, State state) {
|
|
||||||
if (state == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String key = stateKey(state);
|
|
||||||
map.putIfAbsent(key, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void mergeState(java.util.Map<String, State> map, java.util.Collection<State> states) {
|
|
||||||
if (states == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (State state : states) {
|
|
||||||
mergeState(map, state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String stateKey(State state) {
|
|
||||||
if (state.fullIdentifier() != null && !state.fullIdentifier().isBlank()) {
|
|
||||||
return state.fullIdentifier();
|
|
||||||
}
|
|
||||||
return state.rawName();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Stream<String> normalizeStates(Collection<State> states) {
|
private static Stream<String> normalizeStates(Collection<State> states) {
|
||||||
|
|||||||
@@ -113,100 +113,8 @@ public final class AstUtils {
|
|||||||
}
|
}
|
||||||
current = parent;
|
current = parent;
|
||||||
}
|
}
|
||||||
String switchConstraint = buildSwitchCaseConstraint(findEnclosingSwitchCase(node));
|
|
||||||
if (switchConstraint != null) {
|
|
||||||
conditions.add(switchConstraint);
|
|
||||||
}
|
|
||||||
if (conditions.isEmpty()) return null;
|
if (conditions.isEmpty()) return null;
|
||||||
java.util.Collections.reverse(conditions);
|
java.util.Collections.reverse(conditions);
|
||||||
return String.join(" && ", conditions);
|
return String.join(" && ", conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
static org.eclipse.jdt.core.dom.SwitchCase findEnclosingSwitchCase(org.eclipse.jdt.core.dom.ASTNode node) {
|
|
||||||
org.eclipse.jdt.core.dom.ASTNode current = node;
|
|
||||||
while (current != null && !(current instanceof org.eclipse.jdt.core.dom.MethodDeclaration)) {
|
|
||||||
if (current instanceof org.eclipse.jdt.core.dom.SwitchCase switchCase) {
|
|
||||||
return switchCase;
|
|
||||||
}
|
|
||||||
current = current.getParent();
|
|
||||||
}
|
|
||||||
current = node;
|
|
||||||
while (current != null && !(current instanceof org.eclipse.jdt.core.dom.MethodDeclaration)) {
|
|
||||||
if (current instanceof org.eclipse.jdt.core.dom.YieldStatement yieldStatement) {
|
|
||||||
org.eclipse.jdt.core.dom.ASTNode parent = yieldStatement.getParent();
|
|
||||||
if (parent instanceof org.eclipse.jdt.core.dom.SwitchStatement switchStatement) {
|
|
||||||
org.eclipse.jdt.core.dom.SwitchCase switchCase =
|
|
||||||
findSwitchCaseForStatement(switchStatement.statements(), yieldStatement);
|
|
||||||
if (switchCase != null) {
|
|
||||||
return switchCase;
|
|
||||||
}
|
|
||||||
} else if (parent instanceof org.eclipse.jdt.core.dom.SwitchExpression switchExpression) {
|
|
||||||
org.eclipse.jdt.core.dom.SwitchCase switchCase =
|
|
||||||
findSwitchCaseForStatement(switchExpression.statements(), yieldStatement);
|
|
||||||
if (switchCase != null) {
|
|
||||||
return switchCase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (current instanceof org.eclipse.jdt.core.dom.ExpressionStatement expressionStatement) {
|
|
||||||
org.eclipse.jdt.core.dom.ASTNode parent = expressionStatement.getParent();
|
|
||||||
if (parent instanceof org.eclipse.jdt.core.dom.SwitchCase switchCase) {
|
|
||||||
return switchCase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
current = current.getParent();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static org.eclipse.jdt.core.dom.SwitchCase findSwitchCaseForStatement(
|
|
||||||
java.util.List<?> statements, org.eclipse.jdt.core.dom.ASTNode bodyStatement) {
|
|
||||||
int idx = statements.indexOf(bodyStatement);
|
|
||||||
if (idx < 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (int i = idx - 1; i >= 0; i--) {
|
|
||||||
if (statements.get(i) instanceof org.eclipse.jdt.core.dom.SwitchCase switchCase) {
|
|
||||||
return switchCase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String buildSwitchCaseConstraint(org.eclipse.jdt.core.dom.SwitchCase switchCase) {
|
|
||||||
if (switchCase == null || switchCase.isDefault()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
org.eclipse.jdt.core.dom.Expression switchSelector = null;
|
|
||||||
org.eclipse.jdt.core.dom.ASTNode switchParent = switchCase.getParent();
|
|
||||||
if (switchParent instanceof org.eclipse.jdt.core.dom.SwitchStatement ss) {
|
|
||||||
switchSelector = ss.getExpression();
|
|
||||||
} else if (switchParent instanceof org.eclipse.jdt.core.dom.SwitchExpression se) {
|
|
||||||
switchSelector = se.getExpression();
|
|
||||||
}
|
|
||||||
if (switchSelector == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
java.util.List<String> caseLabels = new java.util.ArrayList<>();
|
|
||||||
for (Object exprObj : switchCase.expressions()) {
|
|
||||||
if (exprObj instanceof org.eclipse.jdt.core.dom.Expression caseExpr) {
|
|
||||||
caseLabels.add(caseExpr.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (caseLabels.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String selector = switchSelector.toString();
|
|
||||||
if (caseLabels.size() == 1) {
|
|
||||||
return selector + " == " + caseLabels.get(0);
|
|
||||||
}
|
|
||||||
StringBuilder combined = new StringBuilder("(");
|
|
||||||
for (int i = 0; i < caseLabels.size(); i++) {
|
|
||||||
if (i > 0) {
|
|
||||||
combined.append(" || ");
|
|
||||||
}
|
|
||||||
combined.append(selector).append(" == ").append(caseLabels.get(i));
|
|
||||||
}
|
|
||||||
combined.append(')');
|
|
||||||
return combined.toString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package click.kamil.springstatemachineexporter.ast.common;
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorIndex;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorIndexBuilder;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.LibraryHint;
|
import click.kamil.springstatemachineexporter.analysis.model.LibraryHint;
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver;
|
import click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver;
|
||||||
@@ -24,11 +22,11 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import click.kamil.springstatemachineexporter.spi.SourceContext;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class CodebaseContext {
|
public class CodebaseContext implements SourceContext {
|
||||||
private final Map<String, CompilationUnit> classes = new HashMap<>();
|
private final Map<String, CompilationUnit> classes = new HashMap<>();
|
||||||
private final Map<String, org.eclipse.jdt.core.dom.RecordDeclaration> recordDeclarations = new HashMap<>();
|
|
||||||
private final Map<String, Path> classPaths = new HashMap<>();
|
private final Map<String, Path> classPaths = new HashMap<>();
|
||||||
private final Map<String, String> simpleNameToFqn = new HashMap<>();
|
private final Map<String, String> simpleNameToFqn = new HashMap<>();
|
||||||
private final Set<String> ambiguousSimpleNames = new HashSet<>();
|
private final Set<String> ambiguousSimpleNames = new HashSet<>();
|
||||||
@@ -45,29 +43,20 @@ public class CodebaseContext {
|
|||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
private Path projectRoot;
|
private Path projectRoot;
|
||||||
private final Map<String, Object> cache = new HashMap<>();
|
private final Map<String, Object> cache = new HashMap<>();
|
||||||
private AccessorIndex accessorIndex = AccessorIndex.empty();
|
|
||||||
private boolean inlineAccessors = true;
|
|
||||||
|
|
||||||
public Map<String, Object> getCache() {
|
public Map<String, Object> getCache() {
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccessorIndex getAccessorIndex() {
|
|
||||||
return accessorIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isInlineAccessors() {
|
|
||||||
return inlineAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setInlineAccessors(boolean inlineAccessors) {
|
|
||||||
this.inlineAccessors = inlineAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProjectRoot(Path root) {
|
public void setProjectRoot(Path root) {
|
||||||
this.projectRoot = root;
|
this.projectRoot = root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Path getProjectRoot() {
|
||||||
|
return projectRoot;
|
||||||
|
}
|
||||||
|
|
||||||
public String getRelativePath(Path fullPath) {
|
public String getRelativePath(Path fullPath) {
|
||||||
if (projectRoot != null && fullPath.isAbsolute() && fullPath.startsWith(projectRoot)) {
|
if (projectRoot != null && fullPath.isAbsolute() && fullPath.startsWith(projectRoot)) {
|
||||||
return projectRoot.relativize(fullPath).toString();
|
return projectRoot.relativize(fullPath).toString();
|
||||||
@@ -92,31 +81,7 @@ public class CodebaseContext {
|
|||||||
return Collections.unmodifiableList(libraryHints);
|
return Collections.unmodifiableList(libraryHints);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Set<String> ignorePatterns = new HashSet<>(Set.of(
|
private final Set<String> ignorePatterns = new HashSet<>(Set.of("**/test/**", "**/build/classes/**", "**/build/libs/**", "**/build/tmp/**", "**/build/reports/**", "**/build/test-results/**", "**/node_modules/**", "**/out/**"));
|
||||||
"**/src/test/**",
|
|
||||||
"**/src/test/java/**",
|
|
||||||
"**/src/testFixtures/**",
|
|
||||||
"**/src/test-fixtures/**",
|
|
||||||
"**/target/classes/**",
|
|
||||||
"**/target/test-classes/**",
|
|
||||||
"**/target/generated-test-sources/**",
|
|
||||||
"**/target/surefire/**",
|
|
||||||
"**/target/failsafe/**",
|
|
||||||
"**/target/surefire-reports/**",
|
|
||||||
"**/target/failsafe-reports/**",
|
|
||||||
"**/build/classes/**",
|
|
||||||
"**/build/test-classes/**",
|
|
||||||
"**/build/generated-test-sources/**",
|
|
||||||
"**/build/generated/sources/**/test/**",
|
|
||||||
"**/build/generated/sources/**/testFixtures/**",
|
|
||||||
"**/build/libs/**",
|
|
||||||
"**/build/tmp/**",
|
|
||||||
"**/build/reports/**",
|
|
||||||
"**/build/test-results/**",
|
|
||||||
"**/build/kotlin/**",
|
|
||||||
"**/build/intermediates/**",
|
|
||||||
"**/node_modules/**",
|
|
||||||
"**/out/**"));
|
|
||||||
private String[] classpath = new String[0];
|
private String[] classpath = new String[0];
|
||||||
private String[] sourcepath = new String[0];
|
private String[] sourcepath = new String[0];
|
||||||
private boolean resolveBindings = false;
|
private boolean resolveBindings = false;
|
||||||
@@ -129,6 +94,10 @@ public class CodebaseContext {
|
|||||||
this.classpath = classpath.toArray(new String[0]);
|
this.classpath = classpath.toArray(new String[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getClasspath() {
|
||||||
|
return java.util.Arrays.asList(classpath);
|
||||||
|
}
|
||||||
|
|
||||||
public void setSourcepath(List<String> sourcepath) {
|
public void setSourcepath(List<String> sourcepath) {
|
||||||
this.sourcepath = sourcepath.toArray(new String[0]);
|
this.sourcepath = sourcepath.toArray(new String[0]);
|
||||||
}
|
}
|
||||||
@@ -256,16 +225,6 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.accessorIndex = inlineAccessors
|
|
||||||
? new AccessorIndexBuilder().build(this)
|
|
||||||
: AccessorIndex.empty();
|
|
||||||
if (inlineAccessors) {
|
|
||||||
log.info("Built accessor index: {} trivial accessors across {} types",
|
|
||||||
accessorIndex.trivialCount(), accessorIndex.typeCount());
|
|
||||||
} else {
|
|
||||||
log.info("Accessor inlining disabled; skipping accessor index build.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void indexType(TypeDeclaration td, String parentFqn, Path javaFile) {
|
private void indexType(TypeDeclaration td, String parentFqn, Path javaFile) {
|
||||||
@@ -318,7 +277,6 @@ public class CodebaseContext {
|
|||||||
// Treat as a class since it is a type
|
// Treat as a class since it is a type
|
||||||
classes.put(fqn, (CompilationUnit) rd.getRoot());
|
classes.put(fqn, (CompilationUnit) rd.getRoot());
|
||||||
classPaths.put(fqn, javaFile);
|
classPaths.put(fqn, javaFile);
|
||||||
recordDeclarations.put(fqn, rd);
|
|
||||||
|
|
||||||
if (simpleNameToFqn.containsKey(simpleName)) {
|
if (simpleNameToFqn.containsKey(simpleName)) {
|
||||||
String existingFqn = simpleNameToFqn.get(simpleName);
|
String existingFqn = simpleNameToFqn.get(simpleName);
|
||||||
@@ -647,21 +605,6 @@ public class CodebaseContext {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public org.eclipse.jdt.core.dom.RecordDeclaration getRecordDeclaration(String fqn) {
|
|
||||||
if (fqn == null || fqn.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
org.eclipse.jdt.core.dom.RecordDeclaration indexed = recordDeclarations.get(fqn);
|
|
||||||
if (indexed != null) {
|
|
||||||
return indexed;
|
|
||||||
}
|
|
||||||
AbstractTypeDeclaration abstractType = getAbstractTypeDeclaration(fqn);
|
|
||||||
if (abstractType instanceof org.eclipse.jdt.core.dom.RecordDeclaration recordDeclaration) {
|
|
||||||
return recordDeclaration;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AbstractTypeDeclaration getAbstractTypeDeclaration(String name) {
|
public AbstractTypeDeclaration getAbstractTypeDeclaration(String name) {
|
||||||
// Try exact FQN match first
|
// Try exact FQN match first
|
||||||
CompilationUnit cu = classes.get(name);
|
CompilationUnit cu = classes.get(name);
|
||||||
@@ -862,11 +805,8 @@ public class CodebaseContext {
|
|||||||
Type superclass = td.getSuperclassType();
|
Type superclass = td.getSuperclassType();
|
||||||
if (superclass != null) {
|
if (superclass != null) {
|
||||||
String superclassName = getSimpleName(superclass);
|
String superclassName = getSimpleName(superclass);
|
||||||
if (knownAdapters.contains(superclassName)
|
if (knownAdapters.contains(superclassName))
|
||||||
|| superclass.toString().contains("StateMachineConfigurerAdapter")
|
|
||||||
|| superclass.toString().contains("EnumStateMachineConfigurerAdapter")) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
TypeDeclaration superTd = getTypeDeclaration(superclassName, cu);
|
TypeDeclaration superTd = getTypeDeclaration(superclassName, cu);
|
||||||
if (superTd != null && extendsStateMachineConfigurerAdapterLegacy(superTd, visited))
|
if (superTd != null && extendsStateMachineConfigurerAdapterLegacy(superTd, visited))
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package click.kamil.springstatemachineexporter.ast.common;
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorInlining;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
|
|
||||||
import org.eclipse.jdt.core.dom.*;
|
import org.eclipse.jdt.core.dom.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@@ -21,10 +18,6 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
CURRENT_PATH.set(path);
|
CURRENT_PATH.set(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<String> getCurrentPath() {
|
|
||||||
return CURRENT_PATH.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void clearCurrentPath() {
|
public static void clearCurrentPath() {
|
||||||
CURRENT_PATH.remove();
|
CURRENT_PATH.remove();
|
||||||
}
|
}
|
||||||
@@ -63,7 +56,7 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
Map<IVariableBinding, Expression> paramBindings,
|
Map<IVariableBinding, Expression> paramBindings,
|
||||||
Map<IVariableBinding, Expression> instanceFieldBindings,
|
Map<IVariableBinding, Expression> instanceFieldBindings,
|
||||||
int depth) {
|
int depth) {
|
||||||
if (expr == null || ResolutionBudget.defaults().isDataflowExhausted(depth) || !visited.add(expr)) {
|
if (expr == null || depth > 50 || !visited.add(expr)) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,14 +195,6 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
visited.remove(expr);
|
visited.remove(expr);
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Expression> inlined = tryInlineAccessorInvocation(
|
|
||||||
mi, new HashSet<>(visited), paramBindings, instanceFieldBindings, depth);
|
|
||||||
if (!inlined.isEmpty()) {
|
|
||||||
visited.remove(expr);
|
|
||||||
return inlined;
|
|
||||||
}
|
|
||||||
|
|
||||||
IMethodBinding mb = mi.resolveMethodBinding();
|
IMethodBinding mb = mi.resolveMethodBinding();
|
||||||
if (mb != null) {
|
if (mb != null) {
|
||||||
List<TargetMethod> targets = resolveTargets(mi, mb, visited, paramBindings, instanceFieldBindings, depth);
|
List<TargetMethod> targets = resolveTargets(mi, mb, visited, paramBindings, instanceFieldBindings, depth);
|
||||||
@@ -270,19 +255,6 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
// Handle Super Method Invocations
|
// Handle Super Method Invocations
|
||||||
if (expr instanceof SuperMethodInvocation smi) {
|
if (expr instanceof SuperMethodInvocation smi) {
|
||||||
IMethodBinding mb = smi.resolveMethodBinding();
|
IMethodBinding mb = smi.resolveMethodBinding();
|
||||||
if (mb != null && mb.getDeclaringClass() != null) {
|
|
||||||
String superFqn = mb.getDeclaringClass().getErasure().getQualifiedName();
|
|
||||||
String methodName = smi.getName().getIdentifier();
|
|
||||||
Optional<AccessorSummary> accessor = context.getAccessorIndex().lookup(superFqn, methodName);
|
|
||||||
if (accessor.isPresent() && accessor.get().isGetter()) {
|
|
||||||
Expression fieldValue = readFieldValue(accessor.get(), instanceFieldBindings, smi);
|
|
||||||
if (fieldValue != null) {
|
|
||||||
visited.remove(expr);
|
|
||||||
return getReachingDefinitions(
|
|
||||||
fieldValue, visited, paramBindings, instanceFieldBindings, depth + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (mb != null) {
|
if (mb != null) {
|
||||||
MethodDeclaration md = findMethodDeclaration(mb);
|
MethodDeclaration md = findMethodDeclaration(mb);
|
||||||
if (md != null && md.getBody() != null) {
|
if (md != null && md.getBody() != null) {
|
||||||
@@ -317,457 +289,6 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
return List.of(expr);
|
return List.of(expr);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Expression> tryInlineAccessorInvocation(
|
|
||||||
MethodInvocation mi,
|
|
||||||
Set<ASTNode> visited,
|
|
||||||
Map<IVariableBinding, Expression> paramBindings,
|
|
||||||
Map<IVariableBinding, Expression> instanceFieldBindings,
|
|
||||||
int depth) {
|
|
||||||
if (!context.isInlineAccessors()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
if (ResolutionBudget.defaults().isAccessorInlineExhausted(depth)) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
String methodName = mi.getName().getIdentifier();
|
|
||||||
IMethodBinding methodBinding = mi.resolveMethodBinding();
|
|
||||||
if (methodBinding == null || methodBinding.getDeclaringClass() == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
String declaringFqn = methodBinding.getDeclaringClass().getErasure().getQualifiedName();
|
|
||||||
Optional<AccessorSummary> accessor = context.getAccessorIndex().lookup(declaringFqn, methodName);
|
|
||||||
if (accessor.isEmpty() && !looksLikeIndexedAccessorName(methodName)) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Expression> receiverDefs = List.of();
|
|
||||||
|
|
||||||
if (accessor.isEmpty()) {
|
|
||||||
if (!methodBinding.getDeclaringClass().isInterface()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
Expression receiver = mi.getExpression();
|
|
||||||
if (receiver == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
receiverDefs = getReachingDefinitions(receiver, visited, paramBindings, instanceFieldBindings, depth + 1);
|
|
||||||
accessor = AccessorInlining.lookupAccessor(context, mi, receiverDefs);
|
|
||||||
if (accessor.isEmpty()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (receiverDefs.isEmpty() && mi.getExpression() != null) {
|
|
||||||
receiverDefs = getReachingDefinitions(
|
|
||||||
mi.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accessor.isPresent()) {
|
|
||||||
String accessorOwner = accessor.get().ownerFqn();
|
|
||||||
if (accessorOwner.equals(declaringFqn)) {
|
|
||||||
Optional<AccessorSummary> concreteAccessor =
|
|
||||||
lookupConcreteAccessorOverride(declaringFqn, methodName, receiverDefs);
|
|
||||||
if (concreteAccessor.isPresent()) {
|
|
||||||
accessor = concreteAccessor;
|
|
||||||
} else if (shouldSkipAccessorDueToConcreteOverride(declaringFqn, methodName, receiverDefs)) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accessor.isEmpty()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
if (!accessor.get().isGetter()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accessor.get().kind() == click.kamil.springstatemachineexporter.analysis.index.AccessorKind.CONSTANT_GETTER) {
|
|
||||||
return inlineConstantGetter(accessor.get(), visited, paramBindings, instanceFieldBindings, depth);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accessor.get().kind() == click.kamil.springstatemachineexporter.analysis.index.AccessorKind.RECORD_COMPONENT) {
|
|
||||||
return inlineRecordComponentRead(mi, accessor.get(), receiverDefs, visited, paramBindings, instanceFieldBindings, depth);
|
|
||||||
}
|
|
||||||
|
|
||||||
return inlineAccessorGetter(mi, accessor.get(), receiverDefs, visited, paramBindings, instanceFieldBindings, depth);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Expression> inlineConstantGetter(
|
|
||||||
AccessorSummary accessor,
|
|
||||||
Set<ASTNode> visited,
|
|
||||||
Map<IVariableBinding, Expression> paramBindings,
|
|
||||||
Map<IVariableBinding, Expression> instanceFieldBindings,
|
|
||||||
int depth) {
|
|
||||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(accessor.ownerFqn());
|
|
||||||
if (typeDeclaration == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
MethodDeclaration methodDeclaration = context.findMethodDeclaration(typeDeclaration, accessor.methodName(), true);
|
|
||||||
if (methodDeclaration == null || methodDeclaration.getBody() == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
List<Expression> results = new ArrayList<>();
|
|
||||||
for (Object statementObj : methodDeclaration.getBody().statements()) {
|
|
||||||
if (statementObj instanceof ReturnStatement returnStatement && returnStatement.getExpression() != null) {
|
|
||||||
results.addAll(getReachingDefinitions(
|
|
||||||
returnStatement.getExpression(),
|
|
||||||
visited,
|
|
||||||
paramBindings,
|
|
||||||
instanceFieldBindings,
|
|
||||||
depth + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<AccessorSummary> lookupConcreteAccessorOverride(
|
|
||||||
String declaringFqn,
|
|
||||||
String methodName,
|
|
||||||
List<Expression> receiverDefs) {
|
|
||||||
for (Expression receiverDef : receiverDefs) {
|
|
||||||
String concreteFqn = concreteReceiverTypeFqn(receiverDef);
|
|
||||||
if (concreteFqn == null || concreteFqn.equals(declaringFqn)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Optional<AccessorSummary> concreteAccessor = context.getAccessorIndex().lookup(concreteFqn, methodName);
|
|
||||||
if (concreteAccessor.isPresent() && concreteAccessor.get().isGetter()) {
|
|
||||||
return concreteAccessor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldSkipAccessorDueToConcreteOverride(
|
|
||||||
String declaringFqn,
|
|
||||||
String methodName,
|
|
||||||
List<Expression> receiverDefs) {
|
|
||||||
for (Expression receiverDef : receiverDefs) {
|
|
||||||
String concreteFqn = concreteReceiverTypeFqn(receiverDef);
|
|
||||||
if (concreteFqn == null || concreteFqn.equals(declaringFqn)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
TypeDeclaration concreteType = context.getTypeDeclaration(concreteFqn);
|
|
||||||
if (concreteType != null && context.hasMethod(concreteType, methodName)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String concreteReceiverTypeFqn(Expression receiverDef) {
|
|
||||||
if (receiverDef instanceof ClassInstanceCreation cic) {
|
|
||||||
return AccessorInlining.resolveTypeFqn(cic);
|
|
||||||
}
|
|
||||||
ITypeBinding typeBinding = receiverDef.resolveTypeBinding();
|
|
||||||
if (typeBinding != null) {
|
|
||||||
return typeBinding.getErasure().getQualifiedName();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean looksLikeIndexedAccessorName(String methodName) {
|
|
||||||
return (methodName.startsWith("get") && methodName.length() > 3)
|
|
||||||
|| (methodName.startsWith("set") && methodName.length() > 3)
|
|
||||||
|| (methodName.startsWith("is") && methodName.length() > 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Expression> inlineRecordComponentRead(
|
|
||||||
MethodInvocation mi,
|
|
||||||
AccessorSummary accessor,
|
|
||||||
List<Expression> receiverDefs,
|
|
||||||
Set<ASTNode> visited,
|
|
||||||
Map<IVariableBinding, Expression> paramBindings,
|
|
||||||
Map<IVariableBinding, Expression> instanceFieldBindings,
|
|
||||||
int depth) {
|
|
||||||
Expression receiver = mi.getExpression();
|
|
||||||
List<Expression> candidates = receiverDefs.isEmpty() && receiver != null ? List.of(receiver) : receiverDefs;
|
|
||||||
List<Expression> results = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Expression candidate : candidates) {
|
|
||||||
if (candidate instanceof ClassInstanceCreation cic) {
|
|
||||||
Expression componentValue = readRecordComponentValue(cic, accessor.fieldName());
|
|
||||||
if (componentValue != null) {
|
|
||||||
results.addAll(getReachingDefinitions(componentValue, visited, paramBindings, instanceFieldBindings, depth + 1));
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (candidate instanceof MethodInvocation receiverCall) {
|
|
||||||
List<Expression> nested = tryInlineAccessorInvocation(
|
|
||||||
receiverCall, new HashSet<>(visited), paramBindings, instanceFieldBindings, depth + 1);
|
|
||||||
for (Expression nestedValue : nested) {
|
|
||||||
if (nestedValue instanceof ClassInstanceCreation cic) {
|
|
||||||
Expression componentValue = readRecordComponentValue(cic, accessor.fieldName());
|
|
||||||
if (componentValue != null) {
|
|
||||||
results.addAll(getReachingDefinitions(
|
|
||||||
componentValue, visited, paramBindings, instanceFieldBindings, depth + 1));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
results.addAll(getReachingDefinitions(nestedValue, visited, paramBindings, instanceFieldBindings, depth + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Expression readRecordComponentValue(ClassInstanceCreation cic, String componentName) {
|
|
||||||
org.eclipse.jdt.core.dom.RecordDeclaration record = findRecordDeclaration(AccessorInlining.resolveTypeFqn(cic));
|
|
||||||
if (record == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
int componentIndex = -1;
|
|
||||||
List<?> components = record.recordComponents();
|
|
||||||
for (int i = 0; i < components.size(); i++) {
|
|
||||||
Object componentObj = components.get(i);
|
|
||||||
String name = recordComponentName(componentObj);
|
|
||||||
if (componentName.equals(name)) {
|
|
||||||
componentIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (componentIndex < 0 || componentIndex >= cic.arguments().size()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (Expression) cic.arguments().get(componentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String recordComponentName(Object componentObj) {
|
|
||||||
if (componentObj instanceof SingleVariableDeclaration svd) {
|
|
||||||
return svd.getName().getIdentifier();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
java.lang.reflect.Method getNameMethod = componentObj.getClass().getMethod("getName");
|
|
||||||
Object nameObj = getNameMethod.invoke(componentObj);
|
|
||||||
if (nameObj instanceof SimpleName sn) {
|
|
||||||
return sn.getIdentifier();
|
|
||||||
}
|
|
||||||
} catch (ReflectiveOperationException ignored) {
|
|
||||||
// fall through
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Expression> inlineAccessorGetter(
|
|
||||||
MethodInvocation mi,
|
|
||||||
AccessorSummary accessor,
|
|
||||||
List<Expression> receiverDefs,
|
|
||||||
Set<ASTNode> visited,
|
|
||||||
Map<IVariableBinding, Expression> paramBindings,
|
|
||||||
Map<IVariableBinding, Expression> instanceFieldBindings,
|
|
||||||
int depth) {
|
|
||||||
Expression receiver = mi.getExpression();
|
|
||||||
List<Expression> results = new ArrayList<>();
|
|
||||||
|
|
||||||
if (receiver == null || receiver instanceof ThisExpression) {
|
|
||||||
Expression fieldValue = readFieldValue(accessor, instanceFieldBindings, mi);
|
|
||||||
if (fieldValue != null) {
|
|
||||||
results.addAll(getReachingDefinitions(fieldValue, visited, paramBindings, instanceFieldBindings, depth + 1));
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Expression> candidates = receiverDefs.isEmpty() ? List.of(receiver) : receiverDefs;
|
|
||||||
for (Expression candidate : candidates) {
|
|
||||||
if (candidate instanceof ClassInstanceCreation cic) {
|
|
||||||
Map<IVariableBinding, Expression> fieldBindings =
|
|
||||||
getOrCreateFieldBindings(cic, paramBindings, instanceFieldBindings);
|
|
||||||
|
|
||||||
if (receiver instanceof SimpleName sn) {
|
|
||||||
applyFlowSensitiveMutationsBeforeUse(sn, mi, cic, fieldBindings, paramBindings, visited, depth);
|
|
||||||
} else if (receiver instanceof FieldAccess fa) {
|
|
||||||
applyFlowSensitiveFieldMutationsBeforeUse(fa, mi, fieldBindings, paramBindings, visited, depth);
|
|
||||||
}
|
|
||||||
|
|
||||||
Expression fieldValue = readFieldValue(accessor, fieldBindings, mi);
|
|
||||||
if (fieldValue != null) {
|
|
||||||
results.addAll(getReachingDefinitions(fieldValue, visited, paramBindings, fieldBindings, depth + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyFlowSensitiveMutationsBeforeUse(
|
|
||||||
SimpleName receiverName,
|
|
||||||
MethodInvocation useSite,
|
|
||||||
ClassInstanceCreation creationSite,
|
|
||||||
Map<IVariableBinding, Expression> fieldBindings,
|
|
||||||
Map<IVariableBinding, Expression> paramBindings,
|
|
||||||
Set<ASTNode> visited,
|
|
||||||
int depth) {
|
|
||||||
MethodDeclaration enclosingMethod = findEnclosingMethod(useSite);
|
|
||||||
if (enclosingMethod == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ReachingDefinitions rd = getReachingDefinitionsForMethod(enclosingMethod);
|
|
||||||
if (rd == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Set<ASTNode> defs = rd.getReachingDefinitions(receiverName, receiverName.getIdentifier());
|
|
||||||
for (ASTNode def : defs) {
|
|
||||||
Expression defExpr = ReachingDefinitions.getDefinitionExpression(def);
|
|
||||||
if (defExpr == creationSite || isExpressionWrapping(defExpr, creationSite)) {
|
|
||||||
applyIntermediateMutations(receiverName, def, useSite, enclosingMethod, fieldBindings, paramBindings, visited, depth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyFlowSensitiveFieldMutationsBeforeUse(
|
|
||||||
FieldAccess fieldReceiver,
|
|
||||||
MethodInvocation useSite,
|
|
||||||
Map<IVariableBinding, Expression> fieldBindings,
|
|
||||||
Map<IVariableBinding, Expression> paramBindings,
|
|
||||||
Set<ASTNode> visited,
|
|
||||||
int depth) {
|
|
||||||
Expression qualifier = fieldReceiver.getExpression();
|
|
||||||
if (qualifier != null && !(qualifier instanceof ThisExpression)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
MethodDeclaration enclosingMethod = findEnclosingMethod(useSite);
|
|
||||||
if (enclosingMethod == null || enclosingMethod.getBody() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int useStart = useSite.getStartPosition();
|
|
||||||
String targetFieldName = fieldReceiver.getName().getIdentifier();
|
|
||||||
enclosingMethod.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(MethodInvocation node) {
|
|
||||||
if (node.getStartPosition() >= useStart) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!isThisFieldAccess(node.getExpression(), targetFieldName)) {
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Expression> receiverDefs = getReachingDefinitions(
|
|
||||||
node.getExpression(), visited, paramBindings, fieldBindings, depth + 1);
|
|
||||||
Optional<AccessorSummary> accessor = AccessorInlining.lookupAccessor(context, node, receiverDefs);
|
|
||||||
if (accessor.isPresent() && accessor.get().isSetter()) {
|
|
||||||
inlineAccessorSetter(node, accessor.get(), fieldBindings, paramBindings, visited, depth);
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
IMethodBinding mb = node.resolveMethodBinding();
|
|
||||||
if (mb != null) {
|
|
||||||
List<TargetMethod> targets = resolveTargets(node, mb, visited, paramBindings, fieldBindings, depth + 1);
|
|
||||||
for (TargetMethod target : targets) {
|
|
||||||
if (target.declaration != null) {
|
|
||||||
evaluateMethodMutations(target.declaration, node.arguments(), fieldBindings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isThisFieldAccess(Expression expr, String fieldName) {
|
|
||||||
if (!(expr instanceof FieldAccess fa)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Expression receiver = fa.getExpression();
|
|
||||||
return fa.getName().getIdentifier().equals(fieldName)
|
|
||||||
&& (receiver == null || receiver instanceof ThisExpression);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Expression readFieldValue(
|
|
||||||
AccessorSummary accessor,
|
|
||||||
Map<IVariableBinding, Expression> fieldBindings,
|
|
||||||
ASTNode contextNode) {
|
|
||||||
IVariableBinding fieldBinding = findFieldVariableBinding(accessor.declaringFqn(), accessor.fieldName(), contextNode);
|
|
||||||
if (fieldBinding != null && fieldBindings.containsKey(fieldBinding)) {
|
|
||||||
return fieldBindings.get(fieldBinding);
|
|
||||||
}
|
|
||||||
if (fieldBinding != null) {
|
|
||||||
return findFieldInitializer(fieldBinding, contextNode);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IVariableBinding findFieldVariableBinding(String declaringFqn, String fieldName, ASTNode contextNode) {
|
|
||||||
if (declaringFqn == null || fieldName == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(declaringFqn);
|
|
||||||
if (typeDeclaration != null) {
|
|
||||||
IVariableBinding binding = findFieldOnType(typeDeclaration, fieldName, new HashSet<>());
|
|
||||||
if (binding != null) {
|
|
||||||
return binding;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
org.eclipse.jdt.core.dom.RecordDeclaration recordDeclaration = findRecordDeclaration(declaringFqn);
|
|
||||||
if (recordDeclaration != null) {
|
|
||||||
for (Object componentObj : recordDeclaration.recordComponents()) {
|
|
||||||
if (componentObj instanceof SingleVariableDeclaration component
|
|
||||||
&& component.getName().getIdentifier().equals(fieldName)) {
|
|
||||||
return component.resolveBinding();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IVariableBinding findFieldOnType(TypeDeclaration typeDeclaration, String fieldName, Set<String> visitedTypes) {
|
|
||||||
if (typeDeclaration == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String typeFqn = context.getFqn(typeDeclaration);
|
|
||||||
if (!visitedTypes.add(typeFqn)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (FieldDeclaration fieldDeclaration : typeDeclaration.getFields()) {
|
|
||||||
for (Object fragmentObj : fieldDeclaration.fragments()) {
|
|
||||||
if (fragmentObj instanceof VariableDeclarationFragment fragment
|
|
||||||
&& fragment.getName().getIdentifier().equals(fieldName)) {
|
|
||||||
IVariableBinding binding = fragment.resolveBinding();
|
|
||||||
if (binding != null) {
|
|
||||||
return binding;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String superFqn = context.getSuperclassFqn(typeDeclaration);
|
|
||||||
if (superFqn != null && !"java.lang.Object".equals(superFqn)) {
|
|
||||||
TypeDeclaration superType = context.getTypeDeclaration(superFqn);
|
|
||||||
if (superType != null) {
|
|
||||||
return findFieldOnType(superType, fieldName, visitedTypes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private org.eclipse.jdt.core.dom.RecordDeclaration findRecordDeclaration(String fqn) {
|
|
||||||
return context.getRecordDeclaration(fqn);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void inlineAccessorSetter(
|
|
||||||
MethodInvocation mi,
|
|
||||||
AccessorSummary accessor,
|
|
||||||
Map<IVariableBinding, Expression> fieldBindings,
|
|
||||||
Map<IVariableBinding, Expression> paramBindings,
|
|
||||||
Set<ASTNode> visited,
|
|
||||||
int depth) {
|
|
||||||
if (mi.arguments().isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Expression argument = (Expression) mi.arguments().get(0);
|
|
||||||
List<Expression> resolvedArgument = getReachingDefinitions(argument, visited, paramBindings, fieldBindings, depth + 1);
|
|
||||||
Expression value = resolvedArgument.isEmpty() ? argument : resolvedArgument.get(0);
|
|
||||||
|
|
||||||
IVariableBinding fieldBinding = findFieldVariableBinding(accessor.declaringFqn(), accessor.fieldName(), mi);
|
|
||||||
if (fieldBinding != null) {
|
|
||||||
fieldBindings.put(fieldBinding, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IVariableBinding getVariableBinding(Expression expr) {
|
private IVariableBinding getVariableBinding(Expression expr) {
|
||||||
if (expr instanceof SimpleName sn) {
|
if (expr instanceof SimpleName sn) {
|
||||||
IBinding b = sn.resolveBinding();
|
IBinding b = sn.resolveBinding();
|
||||||
@@ -902,7 +423,7 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
Map<IVariableBinding, Expression> callerParamValues,
|
Map<IVariableBinding, Expression> callerParamValues,
|
||||||
Map<IVariableBinding, Expression> fieldBindings,
|
Map<IVariableBinding, Expression> fieldBindings,
|
||||||
int depth) {
|
int depth) {
|
||||||
if (cb == null || ResolutionBudget.defaults().isConstructorExhausted(depth)) return;
|
if (cb == null || depth > 10) return;
|
||||||
MethodDeclaration cd = findMethodDeclaration(cb);
|
MethodDeclaration cd = findMethodDeclaration(cb);
|
||||||
if (cd == null || cd.getBody() == null) return;
|
if (cd == null || cd.getBody() == null) return;
|
||||||
|
|
||||||
@@ -1179,22 +700,6 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
Map<IVariableBinding, Expression> fieldBindings) {
|
Map<IVariableBinding, Expression> fieldBindings) {
|
||||||
if (md == null || md.getBody() == null) return;
|
if (md == null || md.getBody() == null) return;
|
||||||
|
|
||||||
ASTNode parent = md.getParent();
|
|
||||||
if (parent instanceof AbstractTypeDeclaration typeDeclaration) {
|
|
||||||
String ownerFqn = context.getFqn(typeDeclaration);
|
|
||||||
Optional<AccessorSummary> accessor = context.getAccessorIndex().lookup(ownerFqn, md.getName().getIdentifier());
|
|
||||||
if (accessor.isPresent() && accessor.get().isSetter() && !arguments.isEmpty()) {
|
|
||||||
IVariableBinding fieldBinding = findFieldVariableBinding(
|
|
||||||
accessor.get().declaringFqn(),
|
|
||||||
accessor.get().fieldName(),
|
|
||||||
md);
|
|
||||||
if (fieldBinding != null) {
|
|
||||||
fieldBindings.put(fieldBinding, (Expression) arguments.get(0));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<IVariableBinding, Expression> currentParamValues = new HashMap<>();
|
Map<IVariableBinding, Expression> currentParamValues = new HashMap<>();
|
||||||
List<?> parameters = md.parameters();
|
List<?> parameters = md.parameters();
|
||||||
int count = Math.min(parameters.size(), arguments.size());
|
int count = Math.min(parameters.size(), arguments.size());
|
||||||
@@ -1281,14 +786,6 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
public boolean visit(MethodInvocation mi) {
|
public boolean visit(MethodInvocation mi) {
|
||||||
Expression receiver = mi.getExpression();
|
Expression receiver = mi.getExpression();
|
||||||
if (receiver != null && isTargetVariable(receiver, targetVar, targetName)) {
|
if (receiver != null && isTargetVariable(receiver, targetVar, targetName)) {
|
||||||
List<Expression> receiverDefs = getReachingDefinitions(
|
|
||||||
receiver, visited, paramBindings, fieldBindings, depth + 1);
|
|
||||||
Optional<AccessorSummary> accessor = AccessorInlining.lookupAccessor(context, mi, receiverDefs);
|
|
||||||
if (accessor.isPresent() && accessor.get().isSetter()) {
|
|
||||||
inlineAccessorSetter(mi, accessor.get(), fieldBindings, paramBindings, visited, depth);
|
|
||||||
return super.visit(mi);
|
|
||||||
}
|
|
||||||
|
|
||||||
IMethodBinding mb = mi.resolveMethodBinding();
|
IMethodBinding mb = mi.resolveMethodBinding();
|
||||||
if (mb != null) {
|
if (mb != null) {
|
||||||
List<TargetMethod> targets = resolveTargets(mi, mb, visited, paramBindings, fieldBindings, depth + 1);
|
List<TargetMethod> targets = resolveTargets(mi, mb, visited, paramBindings, fieldBindings, depth + 1);
|
||||||
|
|||||||
@@ -69,26 +69,17 @@ public class StateResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String extractAnnotationValue(Annotation ann) {
|
private String extractAnnotationValue(Annotation ann) {
|
||||||
String raw;
|
|
||||||
if (ann instanceof SingleMemberAnnotation sma) {
|
if (ann instanceof SingleMemberAnnotation sma) {
|
||||||
raw = stripQuotes(sma.getValue().toString());
|
return stripQuotes(sma.getValue().toString());
|
||||||
} else if (ann instanceof NormalAnnotation na) {
|
} else if (ann instanceof NormalAnnotation na) {
|
||||||
raw = null;
|
|
||||||
for (Object pairObj : na.values()) {
|
for (Object pairObj : na.values()) {
|
||||||
MemberValuePair pair = (MemberValuePair) pairObj;
|
MemberValuePair pair = (MemberValuePair) pairObj;
|
||||||
if ("value".equals(pair.getName().getIdentifier())) {
|
if ("value".equals(pair.getName().getIdentifier())) {
|
||||||
raw = stripQuotes(pair.getValue().toString());
|
return stripQuotes(pair.getValue().toString());
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
if (raw != null && raw.contains("${")) {
|
return null;
|
||||||
return click.kamil.springstatemachineexporter.analysis.util.PlaceholderResolver.resolve(
|
|
||||||
raw, java.util.Collections.emptyMap());
|
|
||||||
}
|
|
||||||
return raw;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String stripQuotes(String s) {
|
private String stripQuotes(String s) {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class ExporterCommand implements Callable<Integer> {
|
|||||||
@CommandLine.Spec
|
@CommandLine.Spec
|
||||||
CommandLine.Model.CommandSpec spec;
|
CommandLine.Model.CommandSpec spec;
|
||||||
|
|
||||||
@Option(names = {"-i", "--input"}, description = "Input directory containing the Spring Boot project source code. With -j, optionally rescans sources to resolve machine enum types.")
|
@Option(names = {"-i", "--input"}, description = "Input directory containing the Spring Boot project source code.")
|
||||||
private Path inputDir;
|
private Path inputDir;
|
||||||
|
|
||||||
@Option(names = {"-j", "--json"}, description = "Input JSON file containing the state machine definition.")
|
@Option(names = {"-j", "--json"}, description = "Input JSON file containing the state machine definition.")
|
||||||
@@ -57,11 +57,8 @@ public class ExporterCommand implements Callable<Integer> {
|
|||||||
@Option(names = {"--debug"}, description = "Enable diagnostic mode to show unresolved call chains and detailed analysis logging.", defaultValue = "false")
|
@Option(names = {"--debug"}, description = "Enable diagnostic mode to show unresolved call chains and detailed analysis logging.", defaultValue = "false")
|
||||||
private boolean debug;
|
private boolean debug;
|
||||||
|
|
||||||
@Option(names = {"--include-generated-sources"}, description = "Scan existing Maven target/ and Gradle build/ trees for pre-generated .java sources (MapStruct, annotation processors, etc.). Does not run Maven or Gradle.", negatable = true, defaultValue = "true", fallbackValue = "true")
|
@Option(names = {"--resolve-classpath"}, description = "Automatically run Maven/Gradle to resolve full classpath dependencies for perfect AST bindings.", defaultValue = "false")
|
||||||
private boolean includeGeneratedSources = true;
|
private boolean resolveClasspath;
|
||||||
|
|
||||||
@Option(names = {"--inline-accessors"}, description = "Index trivial JavaBean/record accessors at scan time and inline them during call-chain and constant resolution.", negatable = true, defaultValue = "true", fallbackValue = "true")
|
|
||||||
private boolean inlineAccessors = true;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer call() throws Exception {
|
public Integer call() throws Exception {
|
||||||
@@ -91,9 +88,9 @@ public class ExporterCommand implements Callable<Integer> {
|
|||||||
try {
|
try {
|
||||||
List<String> profiles = activeProfiles != null ? activeProfiles : java.util.Collections.emptyList();
|
List<String> profiles = activeProfiles != null ? activeProfiles : java.util.Collections.emptyList();
|
||||||
if (jsonFile != null) {
|
if (jsonFile != null) {
|
||||||
exportService.runJsonExporter(jsonFile, outputDir, selectedFormats, profiles, eventFormat, stateFormat, inputDir);
|
exportService.runJsonExporter(jsonFile, outputDir, selectedFormats, profiles, eventFormat, stateFormat);
|
||||||
} else {
|
} else {
|
||||||
exportService.runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, profiles, null, null, eventFormat, stateFormat, includeGeneratedSources, inlineAccessors);
|
exportService.runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, profiles, null, null, eventFormat, stateFormat, resolveClasspath);
|
||||||
}
|
}
|
||||||
out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ All state machines have been exported to " + outputDir.toAbsolutePath()));
|
out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ All state machines have been exported to " + outputDir.toAbsolutePath()));
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -20,9 +20,15 @@ public class JsonExporter implements StateMachineExporter {
|
|||||||
@Override
|
@Override
|
||||||
public String export(click.kamil.springstatemachineexporter.analysis.model.AnalysisResult result, ExportOptions options) {
|
public String export(click.kamil.springstatemachineexporter.analysis.model.AnalysisResult result, ExportOptions options) {
|
||||||
try {
|
try {
|
||||||
// Full AnalysisResult serialization: self-contained interchange format for downstream
|
java.util.Map<String, Object> output = new java.util.HashMap<>();
|
||||||
// exporters (HTML, etc.) without requiring source code re-analysis.
|
output.put("name", result.getName());
|
||||||
return objectMapper.writeValueAsString(result);
|
output.put("transitions", result.getTransitions());
|
||||||
|
output.put("startStates", result.getStartStates());
|
||||||
|
output.put("endStates", result.getEndStates());
|
||||||
|
output.put("renderChoicesAsDiamonds", options.isRenderChoicesAsDiamonds());
|
||||||
|
output.put("metadata", result.getMetadata());
|
||||||
|
|
||||||
|
return objectMapper.writeValueAsString(output);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Failed to export to JSON", e);
|
throw new RuntimeException("Failed to export to JSON", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.plugin.java;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.CallGraphEngine;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.JdtCallGraphEngine;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.JdtIntelligenceProvider;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.app.AstTransitionParser;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.app.StateMachineAggregator;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.app.TransitionStateUtils;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
|
import click.kamil.springstatemachineexporter.spi.LanguageAnalyzerPlugin;
|
||||||
|
import click.kamil.springstatemachineexporter.spi.SourceContext;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class JavaLanguageAnalyzerPlugin implements LanguageAnalyzerPlugin {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(Path projectRoot) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SourceContext parseProject(Path projectRoot, List<String> classpath, Set<Path> sourcePaths) {
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setProjectRoot(projectRoot);
|
||||||
|
if (classpath != null && !classpath.isEmpty()) {
|
||||||
|
context.setClasspath(classpath);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path hintsFile = projectRoot.resolve("hints.json");
|
||||||
|
if (!java.nio.file.Files.exists(hintsFile)) {
|
||||||
|
hintsFile = projectRoot.resolve("src/main/resources/hints.json");
|
||||||
|
}
|
||||||
|
if (java.nio.file.Files.exists(hintsFile)) {
|
||||||
|
try {
|
||||||
|
context.loadLibraryHints(hintsFile);
|
||||||
|
} catch (java.io.IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> sp = sourcePaths.stream()
|
||||||
|
.map(p -> p.resolve("src/main/java"))
|
||||||
|
.filter(p -> java.nio.file.Files.exists(p))
|
||||||
|
.map(Path::toString)
|
||||||
|
.toList();
|
||||||
|
if (sp.isEmpty()) {
|
||||||
|
sp = sourcePaths.stream().map(Path::toString).toList();
|
||||||
|
}
|
||||||
|
context.setSourcepath(sp);
|
||||||
|
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
try {
|
||||||
|
context.scan(sourcePaths, Collections.emptySet());
|
||||||
|
} catch (java.io.IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CallGraphEngine createCallGraphEngine(SourceContext context) {
|
||||||
|
if (context instanceof CodebaseContext jdtContext) {
|
||||||
|
return new JdtCallGraphEngine(jdtContext, null);
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Expected CodebaseContext");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CodebaseIntelligenceProvider createIntelligenceProvider(SourceContext context) {
|
||||||
|
if (context instanceof CodebaseContext jdtContext) {
|
||||||
|
return new JdtIntelligenceProvider(jdtContext, jdtContext.getProjectRoot());
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Expected CodebaseContext");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AnalysisResult> extractStateMachines(SourceContext context, boolean renderChoicesAsDiamonds) {
|
||||||
|
if (context instanceof CodebaseContext jdtContext) {
|
||||||
|
List<AnalysisResult> results = new ArrayList<>();
|
||||||
|
|
||||||
|
// 1. Find entry point classes (annotated or extending adapter)
|
||||||
|
List<TypeDeclaration> entryPoints = jdtContext.findEntryPointClasses(List.of("EnableStateMachineFactory", "EnableStateMachine"));
|
||||||
|
for (TypeDeclaration td : entryPoints) {
|
||||||
|
String className = jdtContext.getFqn(td);
|
||||||
|
StateMachineAggregator aggregator = new StateMachineAggregator(jdtContext);
|
||||||
|
List<Transition> transitions = aggregator.aggregateTransitions(td);
|
||||||
|
aggregator.aggregateStates(td);
|
||||||
|
Set<String> initialStatesAst = aggregator.getInitialStates();
|
||||||
|
Set<String> endStatesAst = aggregator.getEndStates();
|
||||||
|
|
||||||
|
Set<String> startStates = TransitionStateUtils.findStartStates(transitions, initialStatesAst);
|
||||||
|
Set<String> endStates = TransitionStateUtils.findEndStates(transitions, endStatesAst);
|
||||||
|
Set<click.kamil.springstatemachineexporter.model.State> allStates = TransitionStateUtils.findAllStates(transitions, initialStatesAst, endStatesAst);
|
||||||
|
|
||||||
|
if (allStates.isEmpty() && transitions.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.add(AnalysisResult.builder()
|
||||||
|
.name(className)
|
||||||
|
.states(allStates)
|
||||||
|
.transitions(transitions)
|
||||||
|
.startStates(startStates)
|
||||||
|
.endStates(endStates)
|
||||||
|
.renderChoicesAsDiamonds(renderChoicesAsDiamonds)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Find methods returning StateMachine beans
|
||||||
|
List<MethodDeclaration> beanMethods = jdtContext.findBeanMethodsReturning(Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory"));
|
||||||
|
for (MethodDeclaration m : beanMethods) {
|
||||||
|
TypeDeclaration parentClass = (TypeDeclaration) m.getParent();
|
||||||
|
String parentFqn = jdtContext.getFqn(parentClass);
|
||||||
|
String methodName = m.getName().getIdentifier();
|
||||||
|
String uniqueName = parentFqn + "#" + methodName;
|
||||||
|
|
||||||
|
List<Transition> transitions = AstTransitionParser.parseTransitions(m, jdtContext);
|
||||||
|
|
||||||
|
Set<String> startStates = TransitionStateUtils.findStartStates(transitions, null);
|
||||||
|
Set<String> endStates = TransitionStateUtils.findEndStates(transitions, null);
|
||||||
|
Set<click.kamil.springstatemachineexporter.model.State> allStates = TransitionStateUtils.findAllStates(transitions, null, null);
|
||||||
|
|
||||||
|
if (allStates.isEmpty() && transitions.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.add(AnalysisResult.builder()
|
||||||
|
.name(uniqueName)
|
||||||
|
.states(allStates)
|
||||||
|
.transitions(transitions)
|
||||||
|
.startStates(startStates)
|
||||||
|
.endStates(endStates)
|
||||||
|
.renderChoicesAsDiamonds(renderChoicesAsDiamonds)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
package click.kamil.springstatemachineexporter.service;
|
package click.kamil.springstatemachineexporter.service;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
import click.kamil.springstatemachineexporter.analysis.enricher.CallChainEnricher;
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
import click.kamil.springstatemachineexporter.analysis.enricher.EntryPointEnricher;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.enricher.PropertyEnricher;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.BusinessFlow;
|
import click.kamil.springstatemachineexporter.analysis.model.BusinessFlow;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.AnalysisResultFinalizer;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
|
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.JdtIntelligenceProvider;
|
import click.kamil.springstatemachineexporter.analysis.service.JdtIntelligenceProvider;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ToolingClasspath;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.app.AstTransitionParser;
|
import click.kamil.springstatemachineexporter.ast.app.AstTransitionParser;
|
||||||
import click.kamil.springstatemachineexporter.ast.app.StateMachineAggregator;
|
import click.kamil.springstatemachineexporter.ast.app.StateMachineAggregator;
|
||||||
import click.kamil.springstatemachineexporter.ast.app.TransitionStateUtils;
|
import click.kamil.springstatemachineexporter.ast.app.TransitionStateUtils;
|
||||||
@@ -20,6 +20,11 @@ import click.kamil.springstatemachineexporter.exporter.StateMachineExporter;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||||
|
import click.kamil.springstatemachineexporter.spi.LanguageAnalyzerPlugin;
|
||||||
|
import click.kamil.springstatemachineexporter.spi.SourceContext;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.CompositeIntelligenceProvider;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.CallGraphEngine;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.JdtCallGraphEngine;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
@@ -40,48 +45,43 @@ public class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ExportService(List<StateMachineExporter> exporters) {
|
public ExportService(List<StateMachineExporter> exporters) {
|
||||||
this(exporters, EnrichmentService.createDefault());
|
this(exporters, new EnrichmentService(List.of(
|
||||||
|
new TriggerEnricher(),
|
||||||
|
new EntryPointEnricher(),
|
||||||
|
new PropertyEnricher(),
|
||||||
|
new CallChainEnricher(),
|
||||||
|
new click.kamil.springstatemachineexporter.analysis.enricher.path.ForwardPathEstimationEnricher(),
|
||||||
|
new click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher()
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final List<String> TARGET_ANNOTATIONS = List.of("EnableStateMachineFactory", "EnableStateMachine");
|
private static final List<String> TARGET_ANNOTATIONS = List.of("EnableStateMachineFactory", "EnableStateMachine");
|
||||||
public static final Set<String> STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory");
|
public static final Set<String> STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory");
|
||||||
|
|
||||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
|
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
|
||||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, Collections.emptyList(), null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, true);
|
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, Collections.emptyList(), null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles) throws IOException {
|
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles) throws IOException {
|
||||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, true);
|
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter) throws IOException {
|
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter) throws IOException {
|
||||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, flowsOverride, machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, true);
|
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, flowsOverride, machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
|
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
|
||||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, eventFormat, stateFormat, true);
|
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, eventFormat, stateFormat, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
|
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat, boolean resolveClasspath) throws IOException {
|
||||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, flowsOverride, machineFilter, eventFormat, stateFormat, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat, boolean includeGeneratedSources) throws IOException {
|
|
||||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, flowsOverride, machineFilter, eventFormat, stateFormat, includeGeneratedSources, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat, boolean includeGeneratedSources, boolean inlineAccessors) throws IOException {
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
CodebaseContext context = new CodebaseContext();
|
||||||
context.setActiveProfiles(activeProfiles);
|
context.setActiveProfiles(activeProfiles);
|
||||||
context.setInlineAccessors(inlineAccessors);
|
|
||||||
|
|
||||||
click.kamil.springstatemachineexporter.analysis.resolver.SiblingDependencyResolver siblingResolver =
|
click.kamil.springstatemachineexporter.analysis.resolver.SiblingDependencyResolver siblingResolver = new click.kamil.springstatemachineexporter.analysis.resolver.SiblingDependencyResolver();
|
||||||
new click.kamil.springstatemachineexporter.analysis.resolver.SiblingDependencyResolver();
|
Set<Path> allPaths = new HashSet<>();
|
||||||
click.kamil.springstatemachineexporter.analysis.resolver.ProjectModuleGraph projectGraph =
|
allPaths.add(inputDir);
|
||||||
siblingResolver.analyzeProject(inputDir);
|
allPaths.addAll(siblingResolver.resolveSiblings(inputDir));
|
||||||
Set<Path> allPaths = projectGraph.resolveScanPaths(inputDir, includeGeneratedSources);
|
|
||||||
log.info("Static module graph discovered {} scan roots from {} (includeGeneratedSources={})",
|
|
||||||
allPaths.size(), inputDir, includeGeneratedSources);
|
|
||||||
|
|
||||||
// Find project root (common ancestor)
|
// Find project root (common ancestor)
|
||||||
Path projectRoot = inputDir.toAbsolutePath();
|
Path projectRoot = inputDir.toAbsolutePath();
|
||||||
@@ -95,28 +95,23 @@ public class ExportService {
|
|||||||
log.info("Project root resolved to: {}", projectRoot);
|
log.info("Project root resolved to: {}", projectRoot);
|
||||||
|
|
||||||
List<String> sourcepaths = allPaths.stream()
|
List<String> sourcepaths = allPaths.stream()
|
||||||
.flatMap(p -> {
|
.map(p -> p.resolve("src/main/java").toString())
|
||||||
List<Path> roots = new ArrayList<>();
|
.filter(p -> Files.exists(Path.of(p)))
|
||||||
Path mainJava = p.resolve("src/main/java");
|
|
||||||
if (Files.exists(mainJava)) {
|
|
||||||
roots.add(mainJava);
|
|
||||||
}
|
|
||||||
if (includeGeneratedSources) {
|
|
||||||
roots.addAll(new click.kamil.springstatemachineexporter.analysis.resolver.GeneratedSourceDiscovery()
|
|
||||||
.discoverSourceRoots(p));
|
|
||||||
}
|
|
||||||
return roots.stream();
|
|
||||||
})
|
|
||||||
.map(Path::toString)
|
|
||||||
.distinct()
|
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
log.info("Setting JDT sourcepath: {}", sourcepaths);
|
log.info("Setting JDT sourcepath: {}", sourcepaths);
|
||||||
context.setSourcepath(sourcepaths);
|
context.setSourcepath(sourcepaths);
|
||||||
List<String> toolingClasspath = ToolingClasspath.currentJvmJarEntries();
|
|
||||||
if (!toolingClasspath.isEmpty()) {
|
if (resolveClasspath) {
|
||||||
context.setClasspath(toolingClasspath);
|
click.kamil.springstatemachineexporter.analysis.service.DynamicClasspathResolver classpathResolver = new click.kamil.springstatemachineexporter.analysis.service.DynamicClasspathResolver();
|
||||||
log.info("Using {} tooling JARs for library type bindings (target project build is not executed).", toolingClasspath.size());
|
List<String> dynamicClasspath = classpathResolver.resolveClasspath(projectRoot);
|
||||||
|
if (!dynamicClasspath.isEmpty()) {
|
||||||
|
context.setClasspath(dynamicClasspath);
|
||||||
|
log.info("Injected {} external JARs into JDT ASTParser", dynamicClasspath.size());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info("Dynamic classpath resolution disabled. Use --resolve-classpath to enable.");
|
||||||
}
|
}
|
||||||
|
|
||||||
context.setResolveBindings(true);
|
context.setResolveBindings(true);
|
||||||
|
|
||||||
Path hintsFile = inputDir.resolve("hints.json");
|
Path hintsFile = inputDir.resolve("hints.json");
|
||||||
@@ -147,8 +142,57 @@ public class ExportService {
|
|||||||
|
|
||||||
context.scan(allPaths, Collections.emptySet());
|
context.scan(allPaths, Collections.emptySet());
|
||||||
|
|
||||||
CodebaseIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, inputDir);
|
// Load SPI plugins dynamically
|
||||||
exportAll(context, intelligence, outputDir, selectedFormats, renderChoicesAsDiamonds, flows, machineFilter, activeProfiles != null ? activeProfiles : Collections.emptyList(), eventFormat, stateFormat);
|
ServiceLoader<LanguageAnalyzerPlugin> loader = ServiceLoader.load(LanguageAnalyzerPlugin.class);
|
||||||
|
List<LanguageAnalyzerPlugin> plugins = new ArrayList<>();
|
||||||
|
for (LanguageAnalyzerPlugin plugin : loader) {
|
||||||
|
if (plugin.supports(inputDir)) {
|
||||||
|
plugins.add(plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (plugins.isEmpty()) {
|
||||||
|
plugins.add(new click.kamil.springstatemachineexporter.plugin.java.JavaLanguageAnalyzerPlugin());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SourceContext> sourceContexts = new ArrayList<>();
|
||||||
|
List<CodebaseIntelligenceProvider> providers = new ArrayList<>();
|
||||||
|
List<CallGraphEngine> engines = new ArrayList<>();
|
||||||
|
List<AnalysisResult> extractedResults = new ArrayList<>();
|
||||||
|
|
||||||
|
for (LanguageAnalyzerPlugin plugin : plugins) {
|
||||||
|
SourceContext sc = plugin.parseProject(projectRoot, context.getClasspath(), allPaths);
|
||||||
|
sourceContexts.add(sc);
|
||||||
|
providers.add(plugin.createIntelligenceProvider(sc));
|
||||||
|
engines.add(plugin.createCallGraphEngine(sc));
|
||||||
|
extractedResults.addAll(plugin.extractStateMachines(sc, renderChoicesAsDiamonds));
|
||||||
|
}
|
||||||
|
|
||||||
|
CodebaseContext codebaseContext = context;
|
||||||
|
CodebaseIntelligenceProvider unifiedIntelligence;
|
||||||
|
if (plugins.size() == 1) {
|
||||||
|
unifiedIntelligence = providers.get(0);
|
||||||
|
if (sourceContexts.get(0) instanceof CodebaseContext jdtCtx) {
|
||||||
|
codebaseContext = jdtCtx;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unifiedIntelligence = new CompositeIntelligenceProvider(providers, engines);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process all extracted state machines
|
||||||
|
for (AnalysisResult result : extractedResults) {
|
||||||
|
String name = result.getName();
|
||||||
|
if (machineFilter != null && !name.contains(machineFilter)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.getFlows() == null || result.getFlows().isEmpty()) {
|
||||||
|
result.setFlows(flows);
|
||||||
|
}
|
||||||
|
|
||||||
|
enrichmentService.enrich(result, codebaseContext, unifiedIntelligence);
|
||||||
|
resolveProperties(result, activeProfiles);
|
||||||
|
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats) throws IOException {
|
public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats) throws IOException {
|
||||||
@@ -160,39 +204,12 @@ public class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
|
public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
|
||||||
runJsonExporter(jsonFile, outputDir, selectedFormats, activeProfiles, eventFormat, stateFormat, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat, Path optionalSourceDir) throws IOException {
|
|
||||||
JsonImportService jsonImportService = new JsonImportService();
|
JsonImportService jsonImportService = new JsonImportService();
|
||||||
AnalysisResult result = jsonImportService.importAnalysisResult(jsonFile);
|
AnalysisResult result = jsonImportService.importAnalysisResult(jsonFile);
|
||||||
|
|
||||||
|
// Apply property resolution pass if placeholders exist
|
||||||
resolveProperties(result, activeProfiles);
|
resolveProperties(result, activeProfiles);
|
||||||
|
|
||||||
CodebaseContext context = null;
|
|
||||||
Path sourceRoot = optionalSourceDir;
|
|
||||||
if (sourceRoot == null) {
|
|
||||||
sourceRoot = click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory
|
|
||||||
.findProjectRoot(jsonFile.toAbsolutePath().getParent());
|
|
||||||
}
|
|
||||||
if (sourceRoot != null) {
|
|
||||||
log.info("JSON re-export: scanning source project at {}", sourceRoot);
|
|
||||||
context = click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory
|
|
||||||
.scanProjectRoot(sourceRoot, activeProfiles);
|
|
||||||
} else {
|
|
||||||
log.info("JSON re-export: no source project found; using embedded machine types if present");
|
|
||||||
}
|
|
||||||
|
|
||||||
enrichmentService.relinkAfterPropertyResolution(result, context, null);
|
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes =
|
|
||||||
click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory.resolveMachineTypes(
|
|
||||||
result.getName(),
|
|
||||||
result.getStateTypeFqn(),
|
|
||||||
result.getEventTypeFqn(),
|
|
||||||
context);
|
|
||||||
AnalysisResultFinalizer.finalizeResult(result, context, machineTypes);
|
|
||||||
|
|
||||||
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
|
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,23 +269,16 @@ public class ExportService {
|
|||||||
StateMachineAggregator aggregator = new StateMachineAggregator(context);
|
StateMachineAggregator aggregator = new StateMachineAggregator(context);
|
||||||
List<Transition> transitions = aggregator.aggregateTransitions(td);
|
List<Transition> transitions = aggregator.aggregateTransitions(td);
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes = StateMachineTypeResolver.resolveTypes(className, context);
|
|
||||||
MachineEnumCanonicalizer.canonicalizeTransitions(transitions, machineTypes);
|
|
||||||
|
|
||||||
aggregator.aggregateStates(td);
|
aggregator.aggregateStates(td);
|
||||||
Set<String> initialStatesAst = MachineEnumCanonicalizer.canonicalizeStateLabels(
|
Set<String> initialStatesAst = aggregator.getInitialStates();
|
||||||
aggregator.getInitialStates(), machineTypes.stateTypeFqn());
|
Set<String> endStatesAst = aggregator.getEndStates();
|
||||||
Set<String> endStatesAst = MachineEnumCanonicalizer.canonicalizeStateLabels(
|
|
||||||
aggregator.getEndStates(), machineTypes.stateTypeFqn());
|
|
||||||
|
|
||||||
log.debug("Start States Ast: {}", initialStatesAst);
|
log.debug("Start States Ast: {}", initialStatesAst);
|
||||||
log.debug("End States Ast: {}", endStatesAst);
|
log.debug("End States Ast: {}", endStatesAst);
|
||||||
|
|
||||||
Set<String> startStates = TransitionStateUtils.findStartStates(transitions, initialStatesAst);
|
Set<String> startStates = TransitionStateUtils.findStartStates(transitions, initialStatesAst);
|
||||||
Set<String> endStates = TransitionStateUtils.findEndStates(transitions, endStatesAst);
|
Set<String> endStates = TransitionStateUtils.findEndStates(transitions, endStatesAst);
|
||||||
Set<click.kamil.springstatemachineexporter.model.State> allStates = MachineEnumCanonicalizer.canonicalizeStates(
|
Set<click.kamil.springstatemachineexporter.model.State> allStates = TransitionStateUtils.findAllStates(transitions, initialStatesAst, endStatesAst);
|
||||||
TransitionStateUtils.findAllStates(transitions, initialStatesAst, endStatesAst),
|
|
||||||
machineTypes.stateTypeFqn());
|
|
||||||
|
|
||||||
if (allStates.isEmpty() && transitions.isEmpty()) {
|
if (allStates.isEmpty() && transitions.isEmpty()) {
|
||||||
log.info("Skipping empty state machine config: {}", className);
|
log.info("Skipping empty state machine config: {}", className);
|
||||||
@@ -285,10 +295,9 @@ public class ExportService {
|
|||||||
.flows(flows)
|
.flows(flows)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
enrichmentService.enrichPreProperty(result, context, intelligence);
|
enrichmentService.enrich(result, context, intelligence);
|
||||||
|
|
||||||
resolveProperties(result, activeProfiles);
|
resolveProperties(result, activeProfiles);
|
||||||
enrichmentService.enrichPostProperty(result, context, intelligence);
|
|
||||||
AnalysisResultFinalizer.finalizeResult(result, context);
|
|
||||||
|
|
||||||
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
|
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
|
||||||
}
|
}
|
||||||
@@ -303,14 +312,9 @@ public class ExportService {
|
|||||||
|
|
||||||
List<Transition> transitions = AstTransitionParser.parseTransitions(m, context);
|
List<Transition> transitions = AstTransitionParser.parseTransitions(m, context);
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes = StateMachineTypeResolver.resolveTypes(parentFqn, context);
|
|
||||||
MachineEnumCanonicalizer.canonicalizeTransitions(transitions, machineTypes);
|
|
||||||
|
|
||||||
Set<String> startStates = TransitionStateUtils.findStartStates(transitions, null);
|
Set<String> startStates = TransitionStateUtils.findStartStates(transitions, null);
|
||||||
Set<String> endStates = TransitionStateUtils.findEndStates(transitions, null);
|
Set<String> endStates = TransitionStateUtils.findEndStates(transitions, null);
|
||||||
Set<click.kamil.springstatemachineexporter.model.State> allStates = MachineEnumCanonicalizer.canonicalizeStates(
|
Set<click.kamil.springstatemachineexporter.model.State> allStates = TransitionStateUtils.findAllStates(transitions, null, null);
|
||||||
TransitionStateUtils.findAllStates(transitions, null, null),
|
|
||||||
machineTypes.stateTypeFqn());
|
|
||||||
|
|
||||||
if (allStates.isEmpty() && transitions.isEmpty()) {
|
if (allStates.isEmpty() && transitions.isEmpty()) {
|
||||||
log.info("Skipping empty state machine bean: {}", uniqueName);
|
log.info("Skipping empty state machine bean: {}", uniqueName);
|
||||||
@@ -327,10 +331,9 @@ public class ExportService {
|
|||||||
.flows(flows)
|
.flows(flows)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
enrichmentService.enrichPreProperty(result, context, intelligence);
|
enrichmentService.enrich(result, context, intelligence);
|
||||||
|
|
||||||
resolveProperties(result, activeProfiles);
|
resolveProperties(result, activeProfiles);
|
||||||
enrichmentService.enrichPostProperty(result, context, intelligence);
|
|
||||||
AnalysisResultFinalizer.finalizeResult(result, context);
|
|
||||||
|
|
||||||
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
|
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.spi;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.CallGraphEngine;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service Provider Interface (SPI) for introducing support for different languages (Java, Kotlin, etc.).
|
||||||
|
*/
|
||||||
|
public interface LanguageAnalyzerPlugin {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if this plugin is capable of handling the provided project root.
|
||||||
|
* Often checks for file extensions (.kt, .java) within the root.
|
||||||
|
*/
|
||||||
|
boolean supports(Path projectRoot);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the codebase and sets up a SourceContext loaded with the language's AST structures.
|
||||||
|
*/
|
||||||
|
SourceContext parseProject(Path projectRoot, List<String> classpath, Set<Path> sourcePaths);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a CallGraphEngine specialized for this language's syntax and paradigms.
|
||||||
|
*/
|
||||||
|
CallGraphEngine createCallGraphEngine(SourceContext context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a CodebaseIntelligenceProvider specialized for this language to discover
|
||||||
|
* Spring State Machine configurations, entry points, and transitions.
|
||||||
|
*/
|
||||||
|
CodebaseIntelligenceProvider createIntelligenceProvider(SourceContext context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds and extracts all Spring State Machine models (configurations and beans) from the project.
|
||||||
|
*/
|
||||||
|
default List<click.kamil.springstatemachineexporter.analysis.model.AnalysisResult> extractStateMachines(SourceContext context, boolean renderChoicesAsDiamonds) {
|
||||||
|
return java.util.Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.spi;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.LibraryHint;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstraction over a parsed source codebase (Java, Kotlin, etc.).
|
||||||
|
* Provides access to common metadata required by analysis engines.
|
||||||
|
*/
|
||||||
|
public interface SourceContext {
|
||||||
|
/**
|
||||||
|
* Gets the root directory of the scanned project.
|
||||||
|
*/
|
||||||
|
Path getProjectRoot();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relativizes an absolute path against the project root.
|
||||||
|
*/
|
||||||
|
String getRelativePath(Path fullPath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a relative file path for a given fully qualified class/type name.
|
||||||
|
*/
|
||||||
|
String getRelativePath(String fqn);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves any loaded library hints (e.g. from hints.json).
|
||||||
|
*/
|
||||||
|
List<LibraryHint> getLibraryHints();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves application properties (e.g. from application.yml).
|
||||||
|
*/
|
||||||
|
Map<String, Map<String, String>> getProperties();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an unstructured cache map that plugins can use to share data across processing phases.
|
||||||
|
*/
|
||||||
|
Map<String, Object> getCache();
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
|
|
||||||
import click.kamil.springstatemachineexporter.service.ExportService;
|
|
||||||
import click.kamil.springstatemachineexporter.service.JsonImportService;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class EnterpriseInitialStateExportTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldExportCanonicalInitialStateForValuePlaceholder(@TempDir Path outputDir) throws Exception {
|
|
||||||
Path inputDir = Path.of("../state_machines/enterprise_order_system").toAbsolutePath().normalize();
|
|
||||||
|
|
||||||
ExportService exportService = new ExportService(List.of(new JsonExporter()));
|
|
||||||
exportService.runExporter(inputDir, outputDir, List.of("json"), true, List.of());
|
|
||||||
|
|
||||||
Path jsonFile = outputDir.resolve("click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig")
|
|
||||||
.resolve("click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig.json");
|
|
||||||
|
|
||||||
var result = new JsonImportService().importAnalysisResult(jsonFile);
|
|
||||||
|
|
||||||
assertThat(result.getStateTypeFqn()).isEqualTo("String");
|
|
||||||
assertThat(result.getStartStates()).containsExactly("String.NEW");
|
|
||||||
assertThat(result.getStates())
|
|
||||||
.anyMatch(s -> "String.NEW".equals(s.fullIdentifier())
|
|
||||||
&& !s.rawName().contains("${"));
|
|
||||||
assertThat(result.getStates())
|
|
||||||
.noneMatch(s -> s.rawName().contains("${order.initial.state"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs the golden JSON consistency audit script as a regression gate.
|
|
||||||
*/
|
|
||||||
class GoldenJsonAuditTest {
|
|
||||||
|
|
||||||
private static Path findProjectRoot() {
|
|
||||||
Path current = Path.of(".").toAbsolutePath();
|
|
||||||
while (current != null && !current.resolve("settings.gradle").toFile().exists()) {
|
|
||||||
current = current.getParent();
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void goldenJsonExportsShouldPassConsistencyAudit() throws Exception {
|
|
||||||
Path moduleRoot = findProjectRoot().resolve("state_machine_exporter");
|
|
||||||
Path script = moduleRoot.resolve("scripts/audit_golden_json.py");
|
|
||||||
assertThat(script).exists();
|
|
||||||
|
|
||||||
Process process = new ProcessBuilder("python3", script.toString())
|
|
||||||
.directory(moduleRoot.toFile())
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start();
|
|
||||||
|
|
||||||
String output;
|
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
|
||||||
output = reader.lines().collect(Collectors.joining("\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(process.waitFor(60, TimeUnit.SECONDS))
|
|
||||||
.as("audit script timed out\n%s", output)
|
|
||||||
.isTrue();
|
|
||||||
assertThat(process.exitValue())
|
|
||||||
.as("audit script failed\n%s", output)
|
|
||||||
.isZero();
|
|
||||||
assertThat(output).contains("Total BUG+WARN: 0");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package click.kamil.springstatemachineexporter;
|
package click.kamil.springstatemachineexporter;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
|
||||||
import click.kamil.springstatemachineexporter.exporter.Dot;
|
import click.kamil.springstatemachineexporter.exporter.Dot;
|
||||||
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
|
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
|
||||||
import click.kamil.springstatemachineexporter.exporter.PlantUml;
|
import click.kamil.springstatemachineexporter.exporter.PlantUml;
|
||||||
@@ -16,6 +17,7 @@ import java.io.ByteArrayOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class GoldenUpdater {
|
public class GoldenUpdater {
|
||||||
@@ -31,16 +33,7 @@ public class GoldenUpdater {
|
|||||||
System.out.println("Updating golden files for: " + scenario.name());
|
System.out.println("Updating golden files for: " + scenario.name());
|
||||||
Path tempDir = Files.createTempDirectory("golden-update-");
|
Path tempDir = Files.createTempDirectory("golden-update-");
|
||||||
try {
|
try {
|
||||||
exportService.runExporter(
|
exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml", "dot", "scxml", "json"), true);
|
||||||
scenario.inputPath(),
|
|
||||||
tempDir,
|
|
||||||
List.of("puml", "dot", "scxml", "json"),
|
|
||||||
true,
|
|
||||||
scenario.activeProfiles(),
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn,
|
|
||||||
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
|
|
||||||
|
|
||||||
List<Path> generatedDirs;
|
List<Path> generatedDirs;
|
||||||
try (var stream = Files.list(tempDir)) {
|
try (var stream = Files.list(tempDir)) {
|
||||||
@@ -166,73 +159,12 @@ public class GoldenUpdater {
|
|||||||
Path.of("src/test/resources/golden/G2StateMachineConfiguration"),
|
Path.of("src/test/resources/golden/G2StateMachineConfiguration"),
|
||||||
"G2StateMachineConfiguration"
|
"G2StateMachineConfiguration"
|
||||||
),
|
),
|
||||||
new TestScenario(
|
|
||||||
"Extended Analysis Sample",
|
|
||||||
Path.of("state_machines/extended_analysis_sample"),
|
|
||||||
Path.of("src/test/resources/golden/ExtendedStateMachineConfig"),
|
|
||||||
"ExtendedStateMachineConfig"
|
|
||||||
),
|
|
||||||
new TestScenario(
|
|
||||||
"Extended Analysis Sample (PROD)",
|
|
||||||
Path.of("state_machines/extended_analysis_sample"),
|
|
||||||
Path.of("src/test/resources/golden/ExtendedStateMachineConfig_PROD"),
|
|
||||||
"ExtendedStateMachineConfig",
|
|
||||||
List.of("prod")
|
|
||||||
),
|
|
||||||
new TestScenario(
|
|
||||||
"Inheritance Sample",
|
|
||||||
Path.of("state_machines/inheritance_sample"),
|
|
||||||
Path.of("src/test/resources/golden/InheritanceStateMachineConfig"),
|
|
||||||
"InheritanceStateMachineConfig"
|
|
||||||
),
|
|
||||||
new TestScenario(
|
|
||||||
"Enterprise Order System",
|
|
||||||
Path.of("state_machines/enterprise_order_system"),
|
|
||||||
Path.of("src/test/resources/golden/EnterpriseStateMachineConfig"),
|
|
||||||
"EnterpriseStateMachineConfig"
|
|
||||||
),
|
|
||||||
new TestScenario(
|
new TestScenario(
|
||||||
"Enterprise Order State Machine",
|
"Enterprise Order State Machine",
|
||||||
Path.of("state_machines/state_machine_enterprise"),
|
Path.of("state_machines/state_machine_enterprise"),
|
||||||
Path.of("src/test/resources/golden/OrderStateMachineConfiguration"),
|
Path.of("src/test/resources/golden/OrderStateMachineConfiguration"),
|
||||||
"OrderStateMachineConfiguration"
|
"OrderStateMachineConfiguration"
|
||||||
),
|
),
|
||||||
new TestScenario(
|
|
||||||
"Multi-Module Sample",
|
|
||||||
Path.of("state_machines/multi_module_sample/core-module"),
|
|
||||||
Path.of("src/test/resources/golden/OrderStateMachineConfig"),
|
|
||||||
"OrderStateMachineConfig"
|
|
||||||
),
|
|
||||||
new TestScenario(
|
|
||||||
"Maven Multi-Module Sample",
|
|
||||||
Path.of("state_machines/maven_multi_module/core-module"),
|
|
||||||
Path.of("src/test/resources/golden/MavenOrderStateMachine"),
|
|
||||||
"MavenOrderStateMachine"
|
|
||||||
),
|
|
||||||
new TestScenario(
|
|
||||||
"Complex Multi-Module Sample",
|
|
||||||
Path.of("state_machines/complex_multi_module_sm"),
|
|
||||||
Path.of("src/test/resources/golden/StateMachineConfig"),
|
|
||||||
"StateMachineConfig"
|
|
||||||
),
|
|
||||||
new TestScenario(
|
|
||||||
"Polymorphic Events Sample",
|
|
||||||
Path.of("state_machines/polymorphic_events_sample"),
|
|
||||||
Path.of("src/test/resources/golden/PolymorphicStateMachineConfiguration"),
|
|
||||||
"PolymorphicStateMachineConfiguration"
|
|
||||||
),
|
|
||||||
new TestScenario(
|
|
||||||
"Layered Dispatcher (Order)",
|
|
||||||
Path.of("state_machines/layered_dispatcher_sample"),
|
|
||||||
Path.of("src/test/resources/golden/StandardOrderStateMachineConfiguration"),
|
|
||||||
"StandardOrderStateMachineConfiguration"
|
|
||||||
),
|
|
||||||
new TestScenario(
|
|
||||||
"Layered Dispatcher (Document)",
|
|
||||||
Path.of("state_machines/layered_dispatcher_sample"),
|
|
||||||
Path.of("src/test/resources/golden/StandardDocumentStateMachineConfiguration"),
|
|
||||||
"StandardDocumentStateMachineConfiguration"
|
|
||||||
),
|
|
||||||
new TestScenario(
|
new TestScenario(
|
||||||
"Enterprise Document State Machine",
|
"Enterprise Document State Machine",
|
||||||
Path.of("state_machines/state_machine_enterprise"),
|
Path.of("state_machines/state_machine_enterprise"),
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package click.kamil.springstatemachineexporter;
|
package click.kamil.springstatemachineexporter;
|
||||||
|
|
||||||
import org.junit.jupiter.api.parallel.Execution;
|
|
||||||
import org.junit.jupiter.api.parallel.ExecutionMode;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
|
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
|
||||||
import click.kamil.springstatemachineexporter.exporter.Dot;
|
import click.kamil.springstatemachineexporter.exporter.Dot;
|
||||||
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
|
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
|
||||||
@@ -23,12 +21,8 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
|
|
||||||
public class RegressionTest {
|
public class RegressionTest {
|
||||||
|
|
||||||
private static final List<StateMachineExporter> EXPORTERS =
|
private final List<StateMachineExporter> exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
|
||||||
List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
|
private final ExportService exportService = new ExportService(exporters);
|
||||||
|
|
||||||
private ExportService exportService() {
|
|
||||||
return new ExportService(EXPORTERS);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Path findProjectRoot() {
|
private static Path findProjectRoot() {
|
||||||
Path current = Path.of(".").toAbsolutePath();
|
Path current = Path.of(".").toAbsolutePath();
|
||||||
@@ -132,12 +126,6 @@ public class RegressionTest {
|
|||||||
Path.of("src/test/resources/golden/EnterpriseStateMachineConfig"),
|
Path.of("src/test/resources/golden/EnterpriseStateMachineConfig"),
|
||||||
"EnterpriseStateMachineConfig"
|
"EnterpriseStateMachineConfig"
|
||||||
),
|
),
|
||||||
new TestScenario(
|
|
||||||
"Enterprise Factory Order Machine",
|
|
||||||
root.resolve("state_machines/state_machine_enterprise"),
|
|
||||||
Path.of("src/test/resources/golden/OrderStateMachineConfiguration"),
|
|
||||||
"OrderStateMachineConfiguration"
|
|
||||||
),
|
|
||||||
new TestScenario(
|
new TestScenario(
|
||||||
"Multi-Module Sample",
|
"Multi-Module Sample",
|
||||||
root.resolve("state_machines/multi_module_sample/core-module"),
|
root.resolve("state_machines/multi_module_sample/core-module"),
|
||||||
@@ -161,44 +149,19 @@ public class RegressionTest {
|
|||||||
root.resolve("state_machines/polymorphic_events_sample"),
|
root.resolve("state_machines/polymorphic_events_sample"),
|
||||||
Path.of("src/test/resources/golden/PolymorphicStateMachineConfiguration"),
|
Path.of("src/test/resources/golden/PolymorphicStateMachineConfiguration"),
|
||||||
"PolymorphicStateMachineConfiguration"
|
"PolymorphicStateMachineConfiguration"
|
||||||
),
|
|
||||||
new TestScenario(
|
|
||||||
"Layered Dispatcher (Order)",
|
|
||||||
root.resolve("state_machines/layered_dispatcher_sample"),
|
|
||||||
Path.of("src/test/resources/golden/StandardOrderStateMachineConfiguration"),
|
|
||||||
"StandardOrderStateMachineConfiguration"
|
|
||||||
),
|
|
||||||
new TestScenario(
|
|
||||||
"Layered Dispatcher (Document)",
|
|
||||||
root.resolve("state_machines/layered_dispatcher_sample"),
|
|
||||||
Path.of("src/test/resources/golden/StandardDocumentStateMachineConfiguration"),
|
|
||||||
"StandardDocumentStateMachineConfiguration"
|
|
||||||
),
|
|
||||||
new TestScenario(
|
|
||||||
"Enterprise Document State Machine",
|
|
||||||
root.resolve("state_machines/state_machine_enterprise"),
|
|
||||||
Path.of("src/test/resources/golden/DocumentStateMachineConfiguration"),
|
|
||||||
"DocumentStateMachineConfiguration"
|
|
||||||
),
|
|
||||||
new TestScenario(
|
|
||||||
"Enterprise User State Machine",
|
|
||||||
root.resolve("state_machines/state_machine_enterprise"),
|
|
||||||
Path.of("src/test/resources/golden/UserStateMachineConfiguration"),
|
|
||||||
"UserStateMachineConfiguration"
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest(name = "{0}")
|
@ParameterizedTest(name = "{0}")
|
||||||
@MethodSource("provideTestScenarios")
|
@MethodSource("provideTestScenarios")
|
||||||
@Execution(ExecutionMode.CONCURRENT)
|
|
||||||
void testOutputMatchesGolden(TestScenario scenario, @TempDir Path tempDir) throws Exception {
|
void testOutputMatchesGolden(TestScenario scenario, @TempDir Path tempDir) throws Exception {
|
||||||
System.out.println("Running test for " + scenario.name());
|
System.out.println("Running test for " + scenario.name());
|
||||||
ExportService exportService = exportService();
|
if (exportService == null) System.out.println("exportService is NULL");
|
||||||
if (scenario.inputPath() == null) System.out.println("inputPath is NULL");
|
if (scenario.inputPath() == null) System.out.println("inputPath is NULL");
|
||||||
if (tempDir == null) System.out.println("tempDir is NULL");
|
if (tempDir == null) System.out.println("tempDir is NULL");
|
||||||
|
|
||||||
exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml", "dot", "scxml", "json"), true, scenario.activeProfiles(), null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
|
exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml", "dot", "scxml", "json"), true, scenario.activeProfiles(), null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, true);
|
||||||
|
|
||||||
// Find the generated directory (it might be named with FQN)
|
// Find the generated directory (it might be named with FQN)
|
||||||
List<Path> generatedDirs;
|
List<Path> generatedDirs;
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class MachineScopeFilterEntryPointTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldKeepOrderEndpointsOnOrderMachineOnly() {
|
|
||||||
EntryPoint orderPay = EntryPoint.builder()
|
|
||||||
.type(EntryPoint.Type.REST)
|
|
||||||
.name("POST /api/orders/pay")
|
|
||||||
.className("click.kamil.examples.statemachine.layered.web.OrderController")
|
|
||||||
.methodName("pay")
|
|
||||||
.metadata(Map.of("path", "/api/orders/pay"))
|
|
||||||
.build();
|
|
||||||
EntryPoint documentSubmit = EntryPoint.builder()
|
|
||||||
.type(EntryPoint.Type.REST)
|
|
||||||
.name("POST /api/documents/submit")
|
|
||||||
.className("click.kamil.examples.statemachine.layered.web.DocumentController")
|
|
||||||
.methodName("submit")
|
|
||||||
.metadata(Map.of("path", "/api/documents/submit"))
|
|
||||||
.build();
|
|
||||||
EntryPoint genericCommand = EntryPoint.builder()
|
|
||||||
.type(EntryPoint.Type.REST)
|
|
||||||
.name("POST /api/commands/{commandKey}")
|
|
||||||
.className("click.kamil.examples.statemachine.layered.web.GenericCommandController")
|
|
||||||
.methodName("execute")
|
|
||||||
.metadata(Map.of("path", "/api/commands/{commandKey}"))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
List<EntryPoint> orderScoped = MachineScopeFilter.filterEntryPointsForMachine(
|
|
||||||
List.of(orderPay, documentSubmit, genericCommand),
|
|
||||||
"click.kamil.examples.statemachine.layered.order.config.StandardOrderStateMachineConfiguration",
|
|
||||||
context);
|
|
||||||
|
|
||||||
assertThat(orderScoped).extracting(EntryPoint::getName)
|
|
||||||
.contains("POST /api/orders/pay", "POST /api/commands/{commandKey}")
|
|
||||||
.doesNotContain("POST /api/documents/submit");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,57 +17,6 @@ class TransitionLinkerEnricherTest {
|
|||||||
|
|
||||||
private final TransitionLinkerEnricher enricher = new TransitionLinkerEnricher();
|
private final TransitionLinkerEnricher enricher = new TransitionLinkerEnricher();
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldLinkWhenPolymorphicEventsContainConcreteEnumConstant() {
|
|
||||||
Transition payT = new Transition();
|
|
||||||
payT.setSourceStates(List.of(State.of("NEW", "OrderState.NEW")));
|
|
||||||
payT.setTargetStates(List.of(State.of("PAID", "OrderState.PAID")));
|
|
||||||
payT.setEvent(Event.of("OrderEvent.PAY", "com.example.OrderEvent.PAY"));
|
|
||||||
|
|
||||||
CallChain chain = CallChain.builder()
|
|
||||||
.triggerPoint(TriggerPoint.builder()
|
|
||||||
.event("e")
|
|
||||||
.polymorphicEvents(List.of("OrderEvent.PAY"))
|
|
||||||
.build())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
AnalysisResult result = AnalysisResult.builder()
|
|
||||||
.name("OrderStateMachineConfig")
|
|
||||||
.transitions(List.of(payT))
|
|
||||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
enricher.enrich(result, null, null);
|
|
||||||
|
|
||||||
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRespectDispatcherBranchConstraintForMachineDomain() {
|
|
||||||
Transition payT = new Transition();
|
|
||||||
payT.setSourceStates(List.of(State.of("NEW", "OrderState.NEW")));
|
|
||||||
payT.setTargetStates(List.of(State.of("PAID", "OrderState.PAID")));
|
|
||||||
payT.setEvent(Event.of("OrderEvent.PAY", "com.example.OrderEvent.PAY"));
|
|
||||||
|
|
||||||
CallChain chain = CallChain.builder()
|
|
||||||
.triggerPoint(TriggerPoint.builder()
|
|
||||||
.event("OrderEvent.PAY")
|
|
||||||
.polymorphicEvents(List.of("OrderEvent.PAY"))
|
|
||||||
.constraint("\"ORDER\".equalsIgnoreCase(type)")
|
|
||||||
.build())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
AnalysisResult result = AnalysisResult.builder()
|
|
||||||
.name("com.example.OrderStateMachineConfig")
|
|
||||||
.transitions(List.of(payT))
|
|
||||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
enricher.enrich(result, null, null);
|
|
||||||
|
|
||||||
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldLinkTransitionByEventOnly() {
|
void shouldLinkTransitionByEventOnly() {
|
||||||
Transition t1 = new Transition();
|
Transition t1 = new Transition();
|
||||||
@@ -194,38 +143,7 @@ class TransitionLinkerEnricherTest {
|
|||||||
assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
|
assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotLinkAllEnumTransitionsWhenConstantNamesCollide() {
|
|
||||||
Transition orderPay = new Transition();
|
|
||||||
orderPay.setSourceStates(List.of(State.of("NEW", "NEW")));
|
|
||||||
orderPay.setTargetStates(List.of(State.of("PAID", "PAID")));
|
|
||||||
orderPay.setEvent(Event.of("PAY", "com.example.OrderEvents.PAY"));
|
|
||||||
|
|
||||||
Transition invoicePay = new Transition();
|
|
||||||
invoicePay.setSourceStates(List.of(State.of("DRAFT", "DRAFT")));
|
|
||||||
invoicePay.setTargetStates(List.of(State.of("SENT", "SENT")));
|
|
||||||
invoicePay.setEvent(Event.of("PAY", "com.example.InvoiceEvents.PAY"));
|
|
||||||
|
|
||||||
CallChain chain = CallChain.builder()
|
|
||||||
.triggerPoint(TriggerPoint.builder()
|
|
||||||
.event("OrderEvents.PAY")
|
|
||||||
.eventTypeFqn("com.example.OrderEvents")
|
|
||||||
.build())
|
|
||||||
.contextMachineId("testMachine")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
AnalysisResult result = AnalysisResult.builder()
|
|
||||||
.name("testMachine")
|
|
||||||
.transitions(List.of(orderPay, invoicePay))
|
|
||||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
enricher.enrich(result, null, null);
|
|
||||||
|
|
||||||
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
|
||||||
assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
|
|
||||||
assertThat(updatedChain.getMatchedTransitions().get(0).getEvent()).isEqualTo("com.example.OrderEvents.PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldMatchBaseEventDespiteScrapedPolyEvents() {
|
void shouldMatchBaseEventDespiteScrapedPolyEvents() {
|
||||||
@@ -594,30 +512,4 @@ class TransitionLinkerEnricherTest {
|
|||||||
enricher.enrich(resultUser, null, null);
|
enricher.enrich(resultUser, null, null);
|
||||||
assertThat(resultUser.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty();
|
assertThat(resultUser.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRejectChainWhenDispatcherDomainConstraintDoesNotMatchMachine() {
|
|
||||||
Transition payT = new Transition();
|
|
||||||
payT.setSourceStates(List.of(State.of("NEW", "OrderState.NEW")));
|
|
||||||
payT.setTargetStates(List.of(State.of("PAID", "OrderState.PAID")));
|
|
||||||
payT.setEvent(Event.of("OrderEvent.PAY", "com.example.OrderEvent.PAY"));
|
|
||||||
|
|
||||||
CallChain chain = CallChain.builder()
|
|
||||||
.triggerPoint(TriggerPoint.builder()
|
|
||||||
.event("OrderEvent.PAY")
|
|
||||||
.polymorphicEvents(List.of("OrderEvent.PAY"))
|
|
||||||
.constraint("\"PAYMENT\".equalsIgnoreCase(type)")
|
|
||||||
.build())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
AnalysisResult result = AnalysisResult.builder()
|
|
||||||
.name("com.example.OrderStateMachineConfig")
|
|
||||||
.transitions(List.of(payT))
|
|
||||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
enricher.enrich(result, null, null);
|
|
||||||
|
|
||||||
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher.matching;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Event;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class StrictFqnMatchingEngineExternalTriggerTest {
|
|
||||||
|
|
||||||
private final StrictFqnMatchingEngine engine = new StrictFqnMatchingEngine();
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldMatchExternalTriggerByMainEventWhenPolymorphicEventsAreEmpty() {
|
|
||||||
Event machineEvent = Event.of(
|
|
||||||
"com.example.order.OrderEvent.PAY",
|
|
||||||
"com.example.order.OrderEvent.PAY");
|
|
||||||
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder()
|
|
||||||
.event("com.example.order.OrderEvent.PAY")
|
|
||||||
.eventTypeFqn("com.example.order.OrderEvent")
|
|
||||||
.external(true)
|
|
||||||
.polymorphicEvents(List.of())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
assertThat(engine.matches(machineEvent, trigger)).isTrue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -142,39 +142,16 @@ class StrictFqnMatchingEngineTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldMatchValueOfWithCorrectEnumWhenPolymorphicEventIsResolved() {
|
void shouldMatchValueOfWithCorrectEnum() {
|
||||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||||
.event("OrderEvents.valueOf(str)")
|
.event("OrderEvents.valueOf(str)")
|
||||||
.eventTypeFqn("com.example.OrderEvents")
|
.eventTypeFqn("com.example.OrderEvents")
|
||||||
.polymorphicEvents(List.of("OrderEvents.PAY"))
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotMatchUnresolvedValueOfWithoutPolymorphicEvents() {
|
|
||||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
|
||||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
|
||||||
.event("OrderEvents.valueOf(str)")
|
|
||||||
.eventTypeFqn("com.example.OrderEvents")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotMatchExternalTriggerWithoutPolymorphicEvents() {
|
|
||||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
|
||||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
|
||||||
.event("event")
|
|
||||||
.external(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldNotMatchValueOfWithIncorrectEnum() {
|
void shouldNotMatchValueOfWithIncorrectEnum() {
|
||||||
Event smEvent = Event.of("PAY", "com.example.InvoiceEvents.PAY");
|
Event smEvent = Event.of("PAY", "com.example.InvoiceEvents.PAY");
|
||||||
@@ -219,38 +196,6 @@ class StrictFqnMatchingEngineTest {
|
|||||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotMatchBareStrippedConstantToAnyEnumWithoutTypeContext() {
|
|
||||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
|
||||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
|
||||||
.event("PAY")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotMatchSameEnumSimpleNameFromDifferentPackages() {
|
|
||||||
Event smEvent = Event.of("PAY", "com.example.other.OrderEvents.PAY");
|
|
||||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
|
||||||
.event("OrderEvents.PAY")
|
|
||||||
.eventTypeFqn("com.example.OrderEvents")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotMatchPolymorphicStrippedConstantWithoutTypeContext() {
|
|
||||||
Event smEvent = Event.of("PAY", "com.example.InvoiceEvents.PAY");
|
|
||||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
|
||||||
.event("getType()")
|
|
||||||
.polymorphicEvents(List.of("PAY"))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldMatchDynamicMethodCallAsVariable() {
|
void shouldMatchDynamicMethodCallAsVariable() {
|
||||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class HeuristicBeanResolutionEngineRoutingTest {
|
|
||||||
|
|
||||||
private final HeuristicBeanResolutionEngine engine = new HeuristicBeanResolutionEngine();
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRejectChainWhenEventTypesDifferEvenIfStateTypesMatch(@TempDir Path tempDir) throws Exception {
|
|
||||||
writeTwoMachineSample(tempDir);
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
CallChain chain = CallChain.builder()
|
|
||||||
.triggerPoint(TriggerPoint.builder()
|
|
||||||
.event("InvoiceEvent.PAY")
|
|
||||||
.eventTypeFqn("com.example.invoice.InvoiceEvent")
|
|
||||||
.stateTypeFqn("com.example.shared.SharedState")
|
|
||||||
.build())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
assertThat(engine.isRoutedToCorrectMachine(
|
|
||||||
chain,
|
|
||||||
"com.example.config.OrderStateMachineConfiguration",
|
|
||||||
context)).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeTwoMachineSample(Path tempDir) throws Exception {
|
|
||||||
Path orderPkg = tempDir.resolve("com/example/order");
|
|
||||||
Path invoicePkg = tempDir.resolve("com/example/invoice");
|
|
||||||
Path sharedPkg = tempDir.resolve("com/example/shared");
|
|
||||||
Path configPkg = tempDir.resolve("com/example/config");
|
|
||||||
Files.createDirectories(orderPkg);
|
|
||||||
Files.createDirectories(invoicePkg);
|
|
||||||
Files.createDirectories(sharedPkg);
|
|
||||||
Files.createDirectories(configPkg);
|
|
||||||
|
|
||||||
Files.writeString(sharedPkg.resolve("SharedState.java"),
|
|
||||||
"package com.example.shared; public enum SharedState { NEW }");
|
|
||||||
Files.writeString(orderPkg.resolve("OrderState.java"),
|
|
||||||
"package com.example.order; public enum OrderState { NEW }");
|
|
||||||
Files.writeString(orderPkg.resolve("OrderEvent.java"),
|
|
||||||
"package com.example.order; public enum OrderEvent { PAY }");
|
|
||||||
Files.writeString(invoicePkg.resolve("InvoiceEvent.java"),
|
|
||||||
"package com.example.invoice; public enum InvoiceEvent { PAY }");
|
|
||||||
Files.writeString(configPkg.resolve("OrderStateMachineConfiguration.java"),
|
|
||||||
"""
|
|
||||||
package com.example.config;
|
|
||||||
import com.example.order.OrderEvent;
|
|
||||||
import com.example.order.OrderState;
|
|
||||||
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
|
|
||||||
public class OrderStateMachineConfiguration
|
|
||||||
extends EnumStateMachineConfigurerAdapter<OrderState, OrderEvent> {
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.junit.jupiter.api.Tag;
|
import org.junit.jupiter.api.Tag;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
@@ -131,38 +127,4 @@ public class HeuristicBeanResolutionEngineTest {
|
|||||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||||
assertTrue(result, "MyStateMachineConfig should not trigger mismatch because it contains StateMachineConfig");
|
assertTrue(result, "MyStateMachineConfig should not trigger mismatch because it contains StateMachineConfig");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRejectAmbiguousChainWhenMultipleStateMachinesExist(@TempDir Path tempDir) throws Exception {
|
|
||||||
Files.writeString(tempDir.resolve("OrderConfig.java"), """
|
|
||||||
package com.example;
|
|
||||||
@org.springframework.statemachine.config.EnableStateMachine
|
|
||||||
public class OrderStateMachineConfig
|
|
||||||
extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<OrderStates, OrderEvents> {}
|
|
||||||
enum OrderStates { S1 }
|
|
||||||
enum OrderEvents { E1 }
|
|
||||||
""");
|
|
||||||
Files.writeString(tempDir.resolve("DocumentConfig.java"), """
|
|
||||||
package com.example;
|
|
||||||
@org.springframework.statemachine.config.EnableStateMachine
|
|
||||||
public class DocumentStateMachineConfig
|
|
||||||
extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<DocumentStates, DocumentEvents> {}
|
|
||||||
enum DocumentStates { D1 }
|
|
||||||
enum DocumentEvents { D1E }
|
|
||||||
""");
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
CallChain chain = CallChain.builder()
|
|
||||||
.methodChain(Arrays.asList("com.example.shared.GlobalGateway.fire()"))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
assertFalse(
|
|
||||||
engine.isRoutedToCorrectMachine(chain, "com.example.OrderStateMachineConfig", context),
|
|
||||||
"Ambiguous shared gateway should not attach to every machine");
|
|
||||||
assertFalse(
|
|
||||||
engine.isRoutedToCorrectMachine(chain, "com.example.DocumentStateMachineConfig", context),
|
|
||||||
"Ambiguous shared gateway should not attach to every machine");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,263 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.junit.jupiter.api.Nested;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class AccessorIndexBuilderTest {
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class ClassAccessors {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldIndexTrivialGetterAndSetter(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Order.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Order {
|
|
||||||
private String event;
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
public void setEvent(String event) { this.event = event; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
AccessorIndex index = scan(tempDir);
|
|
||||||
|
|
||||||
assertThat(index.lookup("com.example.Order", "getEvent")).isPresent();
|
|
||||||
assertThat(index.lookup("com.example.Order", "setEvent")).isPresent();
|
|
||||||
assertThat(index.accessorsForType("com.example.Order")).hasSize(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldIndexNestedClassAccessors(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Outer.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Outer {
|
|
||||||
static class Inner {
|
|
||||||
private String code;
|
|
||||||
public String getCode() { return code; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
AccessorIndex index = scan(tempDir);
|
|
||||||
|
|
||||||
assertThat(index.lookup("com.example.Outer.Inner", "getCode")).isPresent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class Inheritance {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldInheritParentTrivialGetter(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Base.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Base {
|
|
||||||
protected String event;
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Child.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Child extends Base {
|
|
||||||
public void run() {}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
AccessorIndex index = scan(tempDir);
|
|
||||||
|
|
||||||
assertThat(index.lookup("com.example.Child", "getEvent")).isPresent();
|
|
||||||
assertThat(index.lookup("com.example.Child", "getEvent").orElseThrow().ownerFqn())
|
|
||||||
.isEqualTo("com.example.Child");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotInheritWhenChildOverridesWithLogic(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Base.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Base {
|
|
||||||
protected String event;
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Child.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Child extends Base {
|
|
||||||
@Override
|
|
||||||
public String getEvent() {
|
|
||||||
return event == null ? "NONE" : event;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
AccessorIndex index = scan(tempDir);
|
|
||||||
|
|
||||||
assertThat(index.lookup("com.example.Base", "getEvent")).isPresent();
|
|
||||||
assertThat(index.lookup("com.example.Child", "getEvent")).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldPreferChildTrivialOverride(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Base.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Base {
|
|
||||||
protected String event;
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Child.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Child extends Base {
|
|
||||||
@Override
|
|
||||||
public String getEvent() { return this.event; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
AccessorIndex index = scan(tempDir);
|
|
||||||
|
|
||||||
Optional<AccessorSummary> child = index.lookup("com.example.Child", "getEvent");
|
|
||||||
assertThat(child).isPresent();
|
|
||||||
assertThat(child.orElseThrow().ownerFqn()).isEqualTo("com.example.Child");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveFieldDeclaredInSuperclass(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Base.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Base {
|
|
||||||
protected String event;
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Child.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Child extends Base {
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
AccessorIndex index = scan(tempDir);
|
|
||||||
|
|
||||||
AccessorSummary summary = index.lookup("com.example.Child", "getEvent").orElseThrow();
|
|
||||||
assertThat(summary.declaringFqn()).isEqualTo("com.example.Base");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class Records {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldIndexRecordComponents(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Event.java", """
|
|
||||||
package com.example;
|
|
||||||
public record Event(String code, int version) {}
|
|
||||||
""");
|
|
||||||
|
|
||||||
AccessorIndex index = scan(tempDir);
|
|
||||||
|
|
||||||
assertThat(index.lookup("com.example.Event", "code")).isPresent();
|
|
||||||
assertThat(index.lookup("com.example.Event", "code").orElseThrow().kind())
|
|
||||||
.isEqualTo(AccessorKind.RECORD_COMPONENT);
|
|
||||||
assertThat(index.lookup("com.example.Event", "version")).isPresent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class Interfaces {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldIndexDefaultInterfaceAccessor(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/HasEvent.java", """
|
|
||||||
package com.example;
|
|
||||||
public interface HasEvent {
|
|
||||||
default String getEvent() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
AccessorIndex index = scan(tempDir);
|
|
||||||
|
|
||||||
// Default method body is not trivial (returns null literal, not field read)
|
|
||||||
assertThat(index.lookup("com.example.HasEvent", "getEvent")).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldIndexTrivialDefaultInterfaceAccessor(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Holder.java", """
|
|
||||||
package com.example;
|
|
||||||
public interface Holder {
|
|
||||||
String getEvent();
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Impl.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Impl implements Holder {
|
|
||||||
private String event;
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
AccessorIndex index = scan(tempDir);
|
|
||||||
|
|
||||||
assertThat(index.lookup("com.example.Impl", "getEvent")).isPresent();
|
|
||||||
assertThat(index.lookup("com.example.Holder", "getEvent")).isEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class Enums {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldIndexEnumFieldGetter(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Status.java", """
|
|
||||||
package com.example;
|
|
||||||
public enum Status {
|
|
||||||
OK;
|
|
||||||
private final String code = "ok";
|
|
||||||
public String getCode() { return code; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
AccessorIndex index = scan(tempDir);
|
|
||||||
|
|
||||||
assertThat(index.lookup("com.example.Status", "getCode")).isPresent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldBuildViaCodebaseContext(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Demo.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Demo {
|
|
||||||
private String type;
|
|
||||||
public String getType() { return type; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
assertThat(context.getAccessorIndex().lookup("com.example.Demo", "getType")).isPresent();
|
|
||||||
assertThat(context.getAccessorIndex().trivialCount()).isGreaterThan(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static AccessorIndex scan(Path tempDir) throws IOException {
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
return context.getAccessorIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeJava(Path tempDir, String relativePath, String source) throws IOException {
|
|
||||||
Path file = tempDir.resolve(relativePath);
|
|
||||||
Files.createDirectories(file.getParent());
|
|
||||||
Files.writeString(file, source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,678 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorKind;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.DataFlowModel;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel;
|
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
|
||||||
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
|
|
||||||
import org.junit.jupiter.api.Nested;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class AccessorInliningDataFlowTest {
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class GetterInlining {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveTrivialGetterThroughFieldBinding(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public static class Payload {
|
|
||||||
private String event = "SHIP";
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
}
|
|
||||||
public void run() {
|
|
||||||
Payload payload = new Payload();
|
|
||||||
String value = payload.getEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolvedValue(tempDir, "value", "SHIP");
|
|
||||||
assertAccessorIndexed(tempDir, "com.example.Runner.Payload", "getEvent");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveChainedTrivialGetters(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public static class Event {
|
|
||||||
private String code = "PAY";
|
|
||||||
public String getCode() { return code; }
|
|
||||||
}
|
|
||||||
public static class Payload {
|
|
||||||
private Event event = new Event();
|
|
||||||
public Event getEvent() { return event; }
|
|
||||||
}
|
|
||||||
public static class Order {
|
|
||||||
private Payload payload = new Payload();
|
|
||||||
public Payload getPayload() { return payload; }
|
|
||||||
}
|
|
||||||
public void run() {
|
|
||||||
Order order = new Order();
|
|
||||||
String code = order.getPayload().getEvent().getCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolvedValue(tempDir, "code", "PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveInheritedFieldThroughChildGetter(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Base.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Base {
|
|
||||||
protected String event = "BASE";
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Child.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Child extends Base {
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public void run() {
|
|
||||||
Child child = new Child();
|
|
||||||
String value = child.getEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolvedValue(tempDir, "value", "BASE");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveSuperTrivialGetterWithoutOpeningSuperBody(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Base.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Base {
|
|
||||||
protected String event = "SUPER";
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Child.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Child extends Base {
|
|
||||||
public String capture() {
|
|
||||||
return super.getEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public void run() {
|
|
||||||
Child child = new Child();
|
|
||||||
String value = child.capture();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
CodebaseContext context = scan(tempDir);
|
|
||||||
DataFlowModel model = new JdtDataFlowModel(context);
|
|
||||||
Expression initializer = findInitializer(context, "run", "value");
|
|
||||||
String resolved = model.resolveValue(initializer, context);
|
|
||||||
assertThat(resolved).isEqualTo("SUPER");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveRecordComponentAfterNonTrivialGetterWrapper(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Event.java", """
|
|
||||||
package com.example;
|
|
||||||
public record Event(String code) {}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Payload.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Payload {
|
|
||||||
private Event event = new Event("WRAPPED");
|
|
||||||
public Event getEvent() { return event; }
|
|
||||||
public Event getEventViaWrapper() { return getEvent(); }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public void run() {
|
|
||||||
Payload payload = new Payload();
|
|
||||||
String code = payload.getEventViaWrapper().code();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolvedValue(tempDir, "code", "WRAPPED");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveRecordComponentThroughAccessorChain(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Event.java", """
|
|
||||||
package com.example;
|
|
||||||
public record Event(String code) {}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Payload.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Payload {
|
|
||||||
private Event event = new Event("RECORD");
|
|
||||||
public Event getEvent() { return event; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public void run() {
|
|
||||||
Payload payload = new Payload();
|
|
||||||
String code = payload.getEvent().code();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolvedValue(tempDir, "code", "RECORD");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class SetterThenGetter {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldApplySetterMutationBeforeGetterRead(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public static class Command {
|
|
||||||
private String event = "DEFAULT";
|
|
||||||
public void setEvent(String event) { this.event = event; }
|
|
||||||
public String getEvent() { return this.event; }
|
|
||||||
}
|
|
||||||
public void run() {
|
|
||||||
Command cmd = new Command();
|
|
||||||
cmd.setEvent("PAY");
|
|
||||||
String value = cmd.getEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolvedValue(tempDir, "value", "PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldApplySetterThroughInterfaceTypedReceiver(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public interface Command {
|
|
||||||
void setEvent(String event);
|
|
||||||
String getEvent();
|
|
||||||
}
|
|
||||||
public static class Impl implements Command {
|
|
||||||
private String event = "DEFAULT";
|
|
||||||
@Override
|
|
||||||
public void setEvent(String event) { this.event = event; }
|
|
||||||
@Override
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
}
|
|
||||||
public void run() {
|
|
||||||
Command cmd = new Impl();
|
|
||||||
cmd.setEvent("PAY");
|
|
||||||
String value = cmd.getEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolvedValue(tempDir, "value", "PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveSetterArgumentThroughInterfaceTypedReceiver(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public interface Command {
|
|
||||||
void setEvent(String event);
|
|
||||||
String getEvent();
|
|
||||||
}
|
|
||||||
public static class Impl implements Command {
|
|
||||||
private String event = "DEFAULT";
|
|
||||||
@Override
|
|
||||||
public void setEvent(String event) { this.event = event; }
|
|
||||||
@Override
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
}
|
|
||||||
public static class Helper {
|
|
||||||
public String constant() { return "PAY"; }
|
|
||||||
}
|
|
||||||
public void run() {
|
|
||||||
Command cmd = new Impl();
|
|
||||||
Helper helper = new Helper();
|
|
||||||
cmd.setEvent(helper.constant());
|
|
||||||
String value = cmd.getEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolvedValue(tempDir, "value", "PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveFluentSetterChain(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public static class Builder {
|
|
||||||
private String event = "INIT";
|
|
||||||
public Builder setEvent(String event) {
|
|
||||||
this.event = event;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
}
|
|
||||||
public void run() {
|
|
||||||
Builder builder = new Builder();
|
|
||||||
builder.setEvent("FLUENT");
|
|
||||||
String value = builder.getEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolvedValue(tempDir, "value", "FLUENT");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class MustNotBreakExistingResolution {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldStillResolveLiteralReturnWhenGetterIsNotTrivial(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public static class Service {
|
|
||||||
public String getEvent() { return "LITERAL"; }
|
|
||||||
}
|
|
||||||
public void run() {
|
|
||||||
Service service = new Service();
|
|
||||||
String value = service.getEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolvedValue(tempDir, "value", "LITERAL");
|
|
||||||
assertThat(accessorLookup(tempDir, "com.example.Runner.Service", "getEvent"))
|
|
||||||
.isPresent()
|
|
||||||
.get()
|
|
||||||
.satisfies(summary -> assertThat(summary.kind()).isEqualTo(AccessorKind.CONSTANT_GETTER));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldStillResolveDynamicDispatchWhenImplementationReturnsLiteral(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public interface Service {
|
|
||||||
String getEvent();
|
|
||||||
}
|
|
||||||
public static class PayService implements Service {
|
|
||||||
public String getEvent() { return "PAY"; }
|
|
||||||
}
|
|
||||||
public void run() {
|
|
||||||
Service service = new PayService();
|
|
||||||
String value = service.getEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolvedValue(tempDir, "value", "PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotInlineParentAccessorWhenSubclassOverrideIsBlocked(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Base.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Base {
|
|
||||||
protected String event = "base";
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Child.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Child extends Base {
|
|
||||||
@Override
|
|
||||||
public String getEvent() {
|
|
||||||
return "child";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public void run() {
|
|
||||||
Base target = new Child();
|
|
||||||
String value = target.getEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolvedValue(tempDir, "value", "child");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldApplySetterMutationsOnInstanceFieldReceiver(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
private Command command = new Command();
|
|
||||||
public void run() {
|
|
||||||
this.command.setEvent("PAY");
|
|
||||||
String value = this.command.getEvent();
|
|
||||||
}
|
|
||||||
public static class Command {
|
|
||||||
private String event = "DEFAULT";
|
|
||||||
public void setEvent(String event) { this.event = event; }
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolvedValue(tempDir, "value", "PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotApplyParentAccessorIndexWhenChildOverrideIsBlocked(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Base.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Base {
|
|
||||||
protected String event = "BASE";
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Child.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Child extends Base {
|
|
||||||
@Override
|
|
||||||
public String getEvent() {
|
|
||||||
return event == null ? "NONE" : event.toUpperCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public void run() {
|
|
||||||
Child child = new Child();
|
|
||||||
child.event = "pay";
|
|
||||||
String value = child.getEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertThat(accessorLookup(tempDir, "com.example.Child", "getEvent")).isEmpty();
|
|
||||||
assertThat(accessorLookup(tempDir, "com.example.Base", "getEvent")).isPresent();
|
|
||||||
|
|
||||||
CodebaseContext context = scan(tempDir);
|
|
||||||
DataFlowModel model = new JdtDataFlowModel(context);
|
|
||||||
Expression initializer = findInitializer(context, "run", "value");
|
|
||||||
String resolved = model.resolveValue(initializer, context);
|
|
||||||
assertThat(resolved).isNotEqualTo("BASE");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotConfuseMapGetWithBeanGetter(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.HashMap;
|
|
||||||
public class Runner {
|
|
||||||
public void run() {
|
|
||||||
Map<String, String> map = new HashMap<>();
|
|
||||||
map.put("event", "MAP_VALUE");
|
|
||||||
String value = map.get("event");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertThat(accessorLookup(tempDir, "java.util.HashMap", "get")).isEmpty();
|
|
||||||
// Map resolution is separate; ensure we do not crash or return wrong constant.
|
|
||||||
CodebaseContext context = scan(tempDir);
|
|
||||||
DataFlowModel model = new JdtDataFlowModel(context);
|
|
||||||
Expression initializer = findInitializer(context, "run", "value");
|
|
||||||
List<Expression> defs = model.getReachingDefinitions(initializer);
|
|
||||||
assertThat(defs).isNotEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldPreserveBranchSensitiveSetterVisibility(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public static class Command {
|
|
||||||
private String event = "DEFAULT";
|
|
||||||
public void setEvent(String event) { this.event = event; }
|
|
||||||
public String getEvent() { return this.event; }
|
|
||||||
}
|
|
||||||
public void run(boolean pay) {
|
|
||||||
Command cmd = new Command();
|
|
||||||
if (pay) {
|
|
||||||
cmd.setEvent("PAY");
|
|
||||||
} else {
|
|
||||||
cmd.setEvent("CANCEL");
|
|
||||||
}
|
|
||||||
String value = cmd.getEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
CodebaseContext context = scan(tempDir);
|
|
||||||
DataFlowModel model = new JdtDataFlowModel(context);
|
|
||||||
Expression initializer = findInitializer(context, "run", "value");
|
|
||||||
String resolved = model.resolveValue(initializer, context);
|
|
||||||
assertThat(resolved).isIn("PAY", "CANCEL", null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class SchemaStressCases {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldHandleDeepGetterChainWithoutStackOverflow(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public static class L4 { private String v = "DEEP"; public String getV() { return v; } }
|
|
||||||
public static class L3 { private L4 l4 = new L4(); public L4 getL4() { return l4; } }
|
|
||||||
public static class L2 { private L3 l3 = new L3(); public L3 getL3() { return l3; } }
|
|
||||||
public static class L1 { private L2 l2 = new L2(); public L2 getL2() { return l2; } }
|
|
||||||
public void run() {
|
|
||||||
L1 root = new L1();
|
|
||||||
String value = root.getL2().getL3().getL4().getV();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolvedValue(tempDir, "value", "DEEP");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldHandleMixedTrivialAndLiteralGetterChain(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public static class Inner {
|
|
||||||
public String code() { return "INNER"; }
|
|
||||||
}
|
|
||||||
public static class Outer {
|
|
||||||
private Inner inner = new Inner();
|
|
||||||
public Inner getInner() { return inner; }
|
|
||||||
}
|
|
||||||
public void run() {
|
|
||||||
Outer outer = new Outer();
|
|
||||||
String value = outer.getInner().code();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolvedValue(tempDir, "value", "INNER");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveSuperclassFieldViaSubclassThisGetter(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public static class Base {
|
|
||||||
protected String event = "SUPER";
|
|
||||||
}
|
|
||||||
public static class Sub extends Base {
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
}
|
|
||||||
public void run() {
|
|
||||||
Sub sub = new Sub();
|
|
||||||
String value = sub.getEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolvedValue(tempDir, "value", "SUPER");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveInterfaceEnumGetterViaConcreteImplementation(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public interface RichEvent {
|
|
||||||
EventType getType();
|
|
||||||
}
|
|
||||||
public static class PayEvent implements RichEvent {
|
|
||||||
public EventType getType() { return EventType.PAY; }
|
|
||||||
}
|
|
||||||
public void run() {
|
|
||||||
RichEvent event = new PayEvent();
|
|
||||||
EventType value = event.getType();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enum EventType { PAY, SHIP }
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolvedValue(tempDir, "value", "EventType.PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldReturnEmptyWhenAccessorIndexHasNoEntry(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public static class Worker {
|
|
||||||
public String compute() { return "WORK"; }
|
|
||||||
}
|
|
||||||
public void run() {
|
|
||||||
Worker worker = new Worker();
|
|
||||||
String value = worker.compute();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolvedValue(tempDir, "value", "WORK");
|
|
||||||
assertThat(accessorLookup(tempDir, "com.example.Runner.Worker", "compute")).isEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void assertResolvedValue(Path tempDir, String variableName, String expected) throws IOException {
|
|
||||||
CodebaseContext context = scan(tempDir);
|
|
||||||
DataFlowModel model = new JdtDataFlowModel(context);
|
|
||||||
Expression initializer = findInitializer(context, "run", variableName);
|
|
||||||
String resolved = model.resolveValue(initializer, context);
|
|
||||||
assertThat(resolved).isEqualTo(expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void assertAccessorIndexed(Path tempDir, String ownerFqn, String methodName) throws IOException {
|
|
||||||
assertThat(accessorLookup(tempDir, ownerFqn, methodName)).isPresent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static java.util.Optional<AccessorSummary> accessorLookup(Path tempDir, String ownerFqn, String methodName)
|
|
||||||
throws IOException {
|
|
||||||
CodebaseContext context = scan(tempDir);
|
|
||||||
return context.getAccessorIndex().lookup(ownerFqn, methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CodebaseContext scan(Path tempDir) throws IOException {
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.setResolveBindings(true);
|
|
||||||
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
|
||||||
context.scan(tempDir);
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Expression findInitializer(CodebaseContext context, String methodName, String variableName) {
|
|
||||||
for (TypeDeclaration type : context.getTypeDeclarations()) {
|
|
||||||
for (MethodDeclaration method : type.getMethods()) {
|
|
||||||
if (!method.getName().getIdentifier().equals(methodName) || method.getBody() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (Object statementObj : method.getBody().statements()) {
|
|
||||||
if (statementObj instanceof VariableDeclarationStatement statement) {
|
|
||||||
for (Object fragmentObj : statement.fragments()) {
|
|
||||||
if (fragmentObj instanceof VariableDeclarationFragment fragment
|
|
||||||
&& fragment.getName().getIdentifier().equals(variableName)) {
|
|
||||||
return fragment.getInitializer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (TypeDeclaration nested : type.getTypes()) {
|
|
||||||
Expression nestedResult = findInitializerInType(nested, methodName, variableName);
|
|
||||||
if (nestedResult != null) {
|
|
||||||
return nestedResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new AssertionError("Initializer not found for " + variableName + " in " + methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Expression findInitializerInType(TypeDeclaration type, String methodName, String variableName) {
|
|
||||||
for (MethodDeclaration method : type.getMethods()) {
|
|
||||||
if (!method.getName().getIdentifier().equals(methodName) || method.getBody() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (Object statementObj : method.getBody().statements()) {
|
|
||||||
if (statementObj instanceof VariableDeclarationStatement statement) {
|
|
||||||
for (Object fragmentObj : statement.fragments()) {
|
|
||||||
if (fragmentObj instanceof VariableDeclarationFragment fragment
|
|
||||||
&& fragment.getName().getIdentifier().equals(variableName)) {
|
|
||||||
return fragment.getInitializer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (TypeDeclaration nested : type.getTypes()) {
|
|
||||||
Expression result = findInitializerInType(nested, methodName, variableName);
|
|
||||||
if (result != null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeJava(Path tempDir, String relativePath, String source) throws IOException {
|
|
||||||
Path file = tempDir.resolve(relativePath);
|
|
||||||
Files.createDirectories(file.getParent());
|
|
||||||
Files.writeString(file, source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class ConstantExtractorAccessorInliningTest {
|
|
||||||
|
|
||||||
private CodebaseContext context;
|
|
||||||
private ConstantExtractor constantExtractor;
|
|
||||||
private VariableTracer variableTracer;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Payload.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Payload {
|
|
||||||
private String event = "SHIP";
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
public void setEvent(String event) { this.event = event; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public void run() {
|
|
||||||
Payload payload = new Payload();
|
|
||||||
payload.setEvent("PAY");
|
|
||||||
String value = payload.getEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
context = new CodebaseContext();
|
|
||||||
context.setResolveBindings(true);
|
|
||||||
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
constantExtractor = new ConstantExtractor(context, context.getConstantResolver(), mi -> null);
|
|
||||||
variableTracer = new VariableTracer(context, context.getConstantResolver());
|
|
||||||
constantExtractor.setVariableTracer(variableTracer);
|
|
||||||
constantExtractor.setConstructorAnalyzer(new ConstructorAnalyzer(context, context.getConstantResolver()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void resolveMethodReturnConstantShouldReadIndexedGetterField() {
|
|
||||||
List<String> constants = constantExtractor.resolveMethodReturnConstant(
|
|
||||||
"com.example.Payload", "getEvent", 0, new HashSet<>(), null);
|
|
||||||
|
|
||||||
assertThat(constants).contains("SHIP");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void traceLocalSetterShouldUseAccessorIndexFieldName() {
|
|
||||||
Expression result = variableTracer.traceLocalSetter(
|
|
||||||
"com.example.Runner.run", "payload", "getEvent");
|
|
||||||
|
|
||||||
assertThat(result).isNotNull();
|
|
||||||
assertThat(result.toString()).contains("PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void indexedGetterShouldExistForPayload() {
|
|
||||||
assertThat(context.getAccessorIndex().lookup("com.example.Payload", "getEvent")).isPresent();
|
|
||||||
assertThat(context.getAccessorIndex().lookup("com.example.Payload", "setEvent")).isPresent();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void resolveMethodReturnConstantShouldUseImplementationIndexedGetter(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/EventType.java", """
|
|
||||||
package com.example;
|
|
||||||
public interface EventType {
|
|
||||||
String getCode();
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/PayEvent.java", """
|
|
||||||
package com.example;
|
|
||||||
public class PayEvent implements EventType {
|
|
||||||
private String code = "PAY";
|
|
||||||
public String getCode() { return code; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
context = new CodebaseContext();
|
|
||||||
context.setResolveBindings(true);
|
|
||||||
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
|
||||||
context.scan(tempDir);
|
|
||||||
constantExtractor = new ConstantExtractor(context, context.getConstantResolver(), mi -> null);
|
|
||||||
constantExtractor.setConstructorAnalyzer(new ConstructorAnalyzer(context, context.getConstantResolver()));
|
|
||||||
|
|
||||||
List<String> constants = constantExtractor.resolveMethodReturnConstant(
|
|
||||||
"com.example.EventType", "getCode", 0, new HashSet<>(), null);
|
|
||||||
|
|
||||||
assertThat(constants).contains("PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeJava(Path tempDir, String relativePath, String source) throws IOException {
|
|
||||||
Path file = tempDir.resolve(relativePath);
|
|
||||||
Files.createDirectories(file.getParent());
|
|
||||||
Files.writeString(file, source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import org.eclipse.jdt.core.dom.AST;
|
|
||||||
import org.eclipse.jdt.core.dom.ASTParser;
|
|
||||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
import org.junit.jupiter.api.Nested;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class TrivialAccessorDetectorTest {
|
|
||||||
|
|
||||||
private final TrivialAccessorDetector detector = new TrivialAccessorDetector();
|
|
||||||
|
|
||||||
private static final TrivialAccessorDetector.FieldLookup KNOWN_FIELDS =
|
|
||||||
fieldName -> Optional.of("com.example.Demo");
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class Getters {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDetectPlainGetter() {
|
|
||||||
MethodDeclaration md = method("""
|
|
||||||
public class Demo {
|
|
||||||
private String event;
|
|
||||||
public String getEvent() { return this.event; }
|
|
||||||
}
|
|
||||||
""", 0);
|
|
||||||
|
|
||||||
Optional<AccessorSummary> summary = detector.detect("com.example.Demo", md, KNOWN_FIELDS);
|
|
||||||
|
|
||||||
assertThat(summary).isPresent();
|
|
||||||
assertThat(summary.get().kind()).isEqualTo(AccessorKind.GETTER);
|
|
||||||
assertThat(summary.get().fieldName()).isEqualTo("event");
|
|
||||||
assertThat(summary.get().methodName()).isEqualTo("getEvent");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDetectGetterWithoutThisPrefix() {
|
|
||||||
MethodDeclaration md = method("""
|
|
||||||
public class Demo {
|
|
||||||
private String event;
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
}
|
|
||||||
""", 0);
|
|
||||||
|
|
||||||
assertThat(detector.detect("com.example.Demo", md, KNOWN_FIELDS)).isPresent();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDetectBooleanGetter() {
|
|
||||||
MethodDeclaration md = method("""
|
|
||||||
public class Demo {
|
|
||||||
private boolean active;
|
|
||||||
public boolean isActive() { return active; }
|
|
||||||
}
|
|
||||||
""", 0);
|
|
||||||
|
|
||||||
Optional<AccessorSummary> summary = detector.detect("com.example.Demo", md, KNOWN_FIELDS);
|
|
||||||
|
|
||||||
assertThat(summary).isPresent();
|
|
||||||
assertThat(summary.get().kind()).isEqualTo(AccessorKind.BOOLEAN_GETTER);
|
|
||||||
assertThat(summary.get().fieldName()).isEqualTo("active");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDetectGetterWithCast() {
|
|
||||||
MethodDeclaration md = method("""
|
|
||||||
public class Demo {
|
|
||||||
private Object event;
|
|
||||||
public Object getEvent() { return (Object) this.event; }
|
|
||||||
}
|
|
||||||
""", 0);
|
|
||||||
|
|
||||||
assertThat(detector.detect("com.example.Demo", md, KNOWN_FIELDS)).isPresent();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDetectConstantGetter() {
|
|
||||||
MethodDeclaration md = method("""
|
|
||||||
public class Demo {
|
|
||||||
public String getEvent() { return "PAY"; }
|
|
||||||
}
|
|
||||||
""", 0);
|
|
||||||
|
|
||||||
Optional<AccessorSummary> summary = detector.detect("com.example.Demo", md, KNOWN_FIELDS);
|
|
||||||
|
|
||||||
assertThat(summary).isPresent();
|
|
||||||
assertThat(summary.get().kind()).isEqualTo(AccessorKind.CONSTANT_GETTER);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDetectEnumConstantGetter() {
|
|
||||||
MethodDeclaration md = method("""
|
|
||||||
public class Demo {
|
|
||||||
public OrderEvents getType() { return OrderEvents.PAY; }
|
|
||||||
}
|
|
||||||
enum OrderEvents { PAY }
|
|
||||||
""", 0);
|
|
||||||
|
|
||||||
Optional<AccessorSummary> summary = detector.detect("com.example.Demo", md, KNOWN_FIELDS);
|
|
||||||
|
|
||||||
assertThat(summary).isPresent();
|
|
||||||
assertThat(summary.get().kind()).isEqualTo(AccessorKind.CONSTANT_GETTER);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRejectComputedConstantGetter() {
|
|
||||||
MethodDeclaration md = method("""
|
|
||||||
public class Demo {
|
|
||||||
public String getEvent() { return build(); }
|
|
||||||
private String build() { return "X"; }
|
|
||||||
}
|
|
||||||
""", 0);
|
|
||||||
|
|
||||||
assertThat(detector.detect("com.example.Demo", md, KNOWN_FIELDS)).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRejectLazyInitGetter() {
|
|
||||||
MethodDeclaration md = method("""
|
|
||||||
public class Demo {
|
|
||||||
private String event;
|
|
||||||
public String getEvent() {
|
|
||||||
if (event == null) {
|
|
||||||
event = "default";
|
|
||||||
}
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""", 0);
|
|
||||||
|
|
||||||
assertThat(detector.detect("com.example.Demo", md, KNOWN_FIELDS)).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRejectGetterWithMethodCall() {
|
|
||||||
MethodDeclaration md = method("""
|
|
||||||
public class Demo {
|
|
||||||
private String event;
|
|
||||||
public String getEvent() { return event.trim(); }
|
|
||||||
}
|
|
||||||
""", 0);
|
|
||||||
|
|
||||||
assertThat(detector.detect("com.example.Demo", md, KNOWN_FIELDS)).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRejectUnknownField() {
|
|
||||||
MethodDeclaration md = method("""
|
|
||||||
public class Demo {
|
|
||||||
public String getMissing() { return missing; }
|
|
||||||
}
|
|
||||||
""", 0);
|
|
||||||
|
|
||||||
assertThat(detector.detect("com.example.Demo", md, fieldName -> Optional.empty())).isEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class Setters {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDetectVoidSetter() {
|
|
||||||
MethodDeclaration md = method("""
|
|
||||||
public class Demo {
|
|
||||||
private String event;
|
|
||||||
public void setEvent(String event) { this.event = event; }
|
|
||||||
}
|
|
||||||
""", 0);
|
|
||||||
|
|
||||||
Optional<AccessorSummary> summary = detector.detect("com.example.Demo", md, KNOWN_FIELDS);
|
|
||||||
|
|
||||||
assertThat(summary).isPresent();
|
|
||||||
assertThat(summary.get().kind()).isEqualTo(AccessorKind.SETTER);
|
|
||||||
assertThat(summary.get().paramName()).isEqualTo("event");
|
|
||||||
assertThat(summary.get().returnsThis()).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDetectFluentSetter() {
|
|
||||||
MethodDeclaration md = method("""
|
|
||||||
public class Demo {
|
|
||||||
private String event;
|
|
||||||
public Demo setEvent(String event) {
|
|
||||||
this.event = event;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""", 0);
|
|
||||||
|
|
||||||
Optional<AccessorSummary> summary = detector.detect("com.example.Demo", md, KNOWN_FIELDS);
|
|
||||||
|
|
||||||
assertThat(summary).isPresent();
|
|
||||||
assertThat(summary.get().kind()).isEqualTo(AccessorKind.FLUENT_SETTER);
|
|
||||||
assertThat(summary.get().returnsThis()).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRejectSetterWithValidation() {
|
|
||||||
MethodDeclaration md = method("""
|
|
||||||
public class Demo {
|
|
||||||
private String event;
|
|
||||||
public void setEvent(String event) {
|
|
||||||
java.util.Objects.requireNonNull(event);
|
|
||||||
this.event = event;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""", 0);
|
|
||||||
|
|
||||||
assertThat(detector.detect("com.example.Demo", md, KNOWN_FIELDS)).isEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class Records {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDetectRecordComponentAccessor() {
|
|
||||||
Optional<AccessorSummary> summary = detector.detectRecordComponent("com.example.Event", "code");
|
|
||||||
|
|
||||||
assertThat(summary).isPresent();
|
|
||||||
assertThat(summary.get().kind()).isEqualTo(AccessorKind.RECORD_COMPONENT);
|
|
||||||
assertThat(summary.get().methodName()).isEqualTo("code");
|
|
||||||
assertThat(summary.get().fieldName()).isEqualTo("code");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class Naming {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDecapitalizeJavaBeanProperty() {
|
|
||||||
assertThat(TrivialAccessorDetector.decapitalize("Event")).isEqualTo("event");
|
|
||||||
assertThat(TrivialAccessorDetector.decapitalize("URL")).isEqualTo("URL");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MethodDeclaration method(String classSource, int methodIndex) {
|
|
||||||
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
|
|
||||||
parser.setSource(classSource.toCharArray());
|
|
||||||
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
|
||||||
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
|
|
||||||
TypeDeclaration td = (TypeDeclaration) cu.types().get(0);
|
|
||||||
return td.getMethods()[methodIndex];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.model;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class LifecycleTriggerMarkersTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRecognizeLifecycleMarkers() {
|
|
||||||
assertThat(LifecycleTriggerMarkers.isLifecycle("[LIFECYCLE:RESTORE]")).isTrue();
|
|
||||||
assertThat(LifecycleTriggerMarkers.isLifecycle("[LIFECYCLE:RESET]")).isTrue();
|
|
||||||
assertThat(LifecycleTriggerMarkers.isLifecycle("OrderEvent.PAY")).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldFormatDisplayLabels() {
|
|
||||||
assertThat(LifecycleTriggerMarkers.displayLabel("[LIFECYCLE:RESTORE]"))
|
|
||||||
.isEqualTo("Restore persisted state");
|
|
||||||
assertThat(LifecycleTriggerMarkers.displayLabel("[LIFECYCLE:RESET]"))
|
|
||||||
.isEqualTo("Reset state machine");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Nested;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class AccessorPipelineBugFixTest {
|
|
||||||
|
|
||||||
@TempDir
|
|
||||||
Path tempDir;
|
|
||||||
|
|
||||||
CodebaseContext context;
|
|
||||||
ConstantExtractor constantExtractor;
|
|
||||||
ConstructorAnalyzer constructorAnalyzer;
|
|
||||||
AccessorResolver accessorResolver;
|
|
||||||
VariableTracer variableTracer;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() throws IOException {
|
|
||||||
context = new CodebaseContext();
|
|
||||||
context.setResolveBindings(true);
|
|
||||||
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
|
||||||
ConstantResolver constantResolver = context.getConstantResolver();
|
|
||||||
constantExtractor = new ConstantExtractor(context, constantResolver, mi -> null);
|
|
||||||
constructorAnalyzer = new ConstructorAnalyzer(context, constantResolver);
|
|
||||||
variableTracer = new VariableTracer(context, constantResolver);
|
|
||||||
constantExtractor.setVariableTracer(variableTracer);
|
|
||||||
constantExtractor.setConstructorAnalyzer(constructorAnalyzer);
|
|
||||||
accessorResolver = new AccessorResolver(context, constantExtractor, constructorAnalyzer, constantResolver);
|
|
||||||
constantExtractor.setAccessorResolver(accessorResolver);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeJava(String relativePath, String source) throws IOException {
|
|
||||||
Path file = tempDir.resolve(relativePath);
|
|
||||||
Files.createDirectories(file.getParent());
|
|
||||||
Files.writeString(file, source);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void scanWorkspace() throws IOException {
|
|
||||||
context.scan(tempDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class ConstructorAnalyzerSubclassPollution {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotPullSubclassFieldValuesWhenTracingConcreteBaseType() throws IOException {
|
|
||||||
writeJava("com/example/Base.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Base {
|
|
||||||
protected String token;
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava("com/example/Sub.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Sub extends Base {
|
|
||||||
public Sub() { this.token = "SUB_ONLY"; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
scanWorkspace();
|
|
||||||
|
|
||||||
TypeDeclaration baseTd = context.getTypeDeclaration("com.example.Base");
|
|
||||||
List<String> traced = constructorAnalyzer.traceFieldInConstructors(
|
|
||||||
baseTd, "token", context, new HashSet<>(), ResolutionBudget.defaults());
|
|
||||||
|
|
||||||
assertThat(traced).doesNotContain("SUB_ONLY");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class ConstantExtractorAbstractBodyWidening {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldWidenToImplementationsWhenInterfaceMethodHasNoBody() throws IOException {
|
|
||||||
writeJava("com/example/Code.java", """
|
|
||||||
package com.example;
|
|
||||||
public enum Code { A, B }
|
|
||||||
""");
|
|
||||||
writeJava("com/example/RichEvent.java", """
|
|
||||||
package com.example;
|
|
||||||
public interface RichEvent {
|
|
||||||
Code getCode();
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava("com/example/EventA.java", """
|
|
||||||
package com.example;
|
|
||||||
public class EventA implements RichEvent {
|
|
||||||
public Code getCode() { return Code.A; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava("com/example/EventB.java", """
|
|
||||||
package com.example;
|
|
||||||
public class EventB implements RichEvent {
|
|
||||||
public Code getCode() { return Code.B; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
scanWorkspace();
|
|
||||||
|
|
||||||
List<String> resolved = constantExtractor.resolveMethodReturnConstant(
|
|
||||||
"com.example.RichEvent", "getCode", 0, new HashSet<>(), null);
|
|
||||||
|
|
||||||
assertThat(resolved).containsExactlyInAnyOrder("Code.A", "Code.B");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class AccessorResolverCacheKey {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotReuseCachedResultWhenFieldValuesDiffer() throws IOException {
|
|
||||||
writeJava("com/example/Payload.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Payload {
|
|
||||||
private String event;
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
scanWorkspace();
|
|
||||||
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration("com.example.Payload");
|
|
||||||
Map<String, String> valuesA = new HashMap<>(Map.of("event", "PAY"));
|
|
||||||
Map<String, String> valuesB = new HashMap<>(Map.of("event", "CANCEL"));
|
|
||||||
|
|
||||||
List<String> pay = accessorResolver.resolve(
|
|
||||||
"com.example.Payload",
|
|
||||||
"getEvent",
|
|
||||||
new AccessorResolver.ReceiverContext(td, null, null, false, valuesA),
|
|
||||||
ResolutionBudget.defaults());
|
|
||||||
List<String> cancel = accessorResolver.resolve(
|
|
||||||
"com.example.Payload",
|
|
||||||
"getEvent",
|
|
||||||
new AccessorResolver.ReceiverContext(td, null, null, false, valuesB),
|
|
||||||
ResolutionBudget.defaults());
|
|
||||||
|
|
||||||
assertThat(pay).containsExactly("PAY");
|
|
||||||
assertThat(cancel).containsExactly("CANCEL");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class InlineAccessorsDisabled {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldSkipIndexedDataflowShortcutWhenDisabled() throws IOException {
|
|
||||||
writeJava("com/example/Payload.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Payload {
|
|
||||||
private String event = "INLINE";
|
|
||||||
public String getEvent() { return event; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
context.setInlineAccessors(false);
|
|
||||||
scanWorkspace();
|
|
||||||
|
|
||||||
assertThat(context.getAccessorIndex().trivialCount()).isZero();
|
|
||||||
assertThat(context.isInlineAccessors()).isFalse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ExpressionAccessClassifier;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.HeuristicCallGraphEngine;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Nested;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class PipelineRefactorTest {
|
|
||||||
|
|
||||||
@TempDir
|
|
||||||
Path tempDir;
|
|
||||||
|
|
||||||
CodebaseContext context;
|
|
||||||
ConstantExtractor constantExtractor;
|
|
||||||
ConstructorAnalyzer constructorAnalyzer;
|
|
||||||
AccessorResolver accessorResolver;
|
|
||||||
ExpressionAccessClassifier expressionAccessClassifier;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() throws IOException {
|
|
||||||
context = new CodebaseContext();
|
|
||||||
context.setResolveBindings(true);
|
|
||||||
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
|
||||||
ConstantResolver constantResolver = context.getConstantResolver();
|
|
||||||
constantExtractor = new ConstantExtractor(context, constantResolver, mi -> null);
|
|
||||||
constructorAnalyzer = new ConstructorAnalyzer(context, constantResolver);
|
|
||||||
VariableTracer variableTracer = new VariableTracer(context, constantResolver);
|
|
||||||
constantExtractor.setVariableTracer(variableTracer);
|
|
||||||
constantExtractor.setConstructorAnalyzer(constructorAnalyzer);
|
|
||||||
constructorAnalyzer.setVariableTracer(variableTracer);
|
|
||||||
constructorAnalyzer.setConstantExtractor(constantExtractor);
|
|
||||||
expressionAccessClassifier = new ExpressionAccessClassifier(context, variableTracer, constantResolver, expr -> null);
|
|
||||||
constantExtractor.setExpressionAccessClassifier(expressionAccessClassifier);
|
|
||||||
accessorResolver = new AccessorResolver(context, constantExtractor, constructorAnalyzer, constantResolver);
|
|
||||||
constantExtractor.setAccessorResolver(accessorResolver);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeJava(String relativePath, String source) throws IOException {
|
|
||||||
Path file = tempDir.resolve(relativePath);
|
|
||||||
Files.createDirectories(file.getParent());
|
|
||||||
Files.writeString(file, source);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void scanWorkspace() throws IOException {
|
|
||||||
context.scan(tempDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class ResolutionBudgetTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldUseSharedDefaults() {
|
|
||||||
ResolutionBudget budget = ResolutionBudget.defaults();
|
|
||||||
assertThat(budget.isMethodReturnExhausted(20)).isFalse();
|
|
||||||
assertThat(budget.isMethodReturnExhausted(21)).isTrue();
|
|
||||||
assertThat(budget.isUnwrapExhausted(5)).isFalse();
|
|
||||||
assertThat(budget.isUnwrapExhausted(6)).isTrue();
|
|
||||||
assertThat(budget.allowGlobalFieldScan()).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDecrementDepthLimitsViaChild() {
|
|
||||||
ResolutionBudget child = ResolutionBudget.defaults().child();
|
|
||||||
assertThat(child.isMethodReturnExhausted(19)).isFalse();
|
|
||||||
assertThat(child.isMethodReturnExhausted(20)).isTrue();
|
|
||||||
assertThat(child.isDataflowExhausted(49)).isFalse();
|
|
||||||
assertThat(child.isDataflowExhausted(50)).isTrue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class LibraryUnwrapRegistryTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRecognizeReactiveReceiversAndFactoryMethods() {
|
|
||||||
assertThat(LibraryUnwrapRegistry.isUnwrapReceiverType("Mono")).isTrue();
|
|
||||||
assertThat(LibraryUnwrapRegistry.isUnwrapMethodName("just")).isTrue();
|
|
||||||
assertThat(LibraryUnwrapRegistry.shouldStopUnwrap("buildPayload")).isTrue();
|
|
||||||
assertThat(LibraryUnwrapRegistry.isEventSetterMethodName("eventType")).isTrue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class MethodInvocationUnwrapperTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldPeelOptionalOfFactoryCall() throws IOException {
|
|
||||||
writeJava("com/example/Factory.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Factory {
|
|
||||||
public static String build() {
|
|
||||||
return Optional.of("PAY").get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
scanWorkspace();
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration("com.example.Factory");
|
|
||||||
assertThat(td).isNotNull();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class AccessorResolverTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveConstantGetterWithoutCic() throws IOException {
|
|
||||||
writeJava("com/example/Events.java", """
|
|
||||||
package com.example;
|
|
||||||
public enum Events { PAY, CANCEL }
|
|
||||||
""");
|
|
||||||
writeJava("com/example/EventHolder.java", """
|
|
||||||
package com.example;
|
|
||||||
public class EventHolder {
|
|
||||||
public Events getEvent() { return Events.PAY; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
scanWorkspace();
|
|
||||||
List<String> resolved = accessorResolver.resolve(
|
|
||||||
"com.example.EventHolder",
|
|
||||||
"getEvent",
|
|
||||||
new AccessorResolver.ReceiverContext(null, null, null, false, null),
|
|
||||||
ResolutionBudget.defaults());
|
|
||||||
assertThat(resolved).contains("Events.PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldCacheRepeatedLookups() throws IOException {
|
|
||||||
writeJava("com/example/Events.java", """
|
|
||||||
package com.example;
|
|
||||||
public enum Events { PAY }
|
|
||||||
""");
|
|
||||||
writeJava("com/example/EventHolder.java", """
|
|
||||||
package com.example;
|
|
||||||
public class EventHolder {
|
|
||||||
public Events getEvent() { return Events.PAY; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
scanWorkspace();
|
|
||||||
AccessorResolver.ReceiverContext receiverContext =
|
|
||||||
new AccessorResolver.ReceiverContext(null, null, null, false, null);
|
|
||||||
List<String> first = accessorResolver.resolve(
|
|
||||||
"com.example.EventHolder", "getEvent", receiverContext, ResolutionBudget.defaults());
|
|
||||||
List<String> second = accessorResolver.resolve(
|
|
||||||
"com.example.EventHolder", "getEvent", receiverContext, ResolutionBudget.defaults());
|
|
||||||
assertThat(first).contains("Events.PAY");
|
|
||||||
assertThat(second).isSameAs(first);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveGetterChainStepViaIndexBeforeReturnedCic() throws IOException {
|
|
||||||
writeJava("com/example/Payload.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Payload {
|
|
||||||
public String getType() { return "PAY"; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava("com/example/Inner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Inner {
|
|
||||||
private final Payload payload = new Payload();
|
|
||||||
public Payload getPayload() { return payload; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
scanWorkspace();
|
|
||||||
AccessorResolver.GetterChainStep step = accessorResolver.resolveGetterChainStep(
|
|
||||||
"com.example.Inner", "getPayload", ResolutionBudget.defaults());
|
|
||||||
assertThat(step).isNotNull();
|
|
||||||
assertThat(step.typeFqn()).contains("Payload");
|
|
||||||
assertThat(step.cic()).isNotNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveFactoryBuildThenGetterWithoutCicDeadEnd() throws IOException {
|
|
||||||
writeJava("com/example/Events.java", """
|
|
||||||
package com.example;
|
|
||||||
public enum Events { PAY }
|
|
||||||
""");
|
|
||||||
writeJava("com/example/Order.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Order {
|
|
||||||
public Events getEvent() { return Events.PAY; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava("com/example/OrderFactory.java", """
|
|
||||||
package com.example;
|
|
||||||
public class OrderFactory {
|
|
||||||
public Order build() { return new Order(); }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava("com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public void run() {
|
|
||||||
OrderFactory factory = new OrderFactory();
|
|
||||||
Order order = factory.build();
|
|
||||||
Events event = order.getEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
scanWorkspace();
|
|
||||||
List<String> events = accessorResolver.resolve(
|
|
||||||
"com.example.Order",
|
|
||||||
"getEvent",
|
|
||||||
new AccessorResolver.ReceiverContext(
|
|
||||||
context.getTypeDeclaration("com.example.Order"),
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
null),
|
|
||||||
ResolutionBudget.defaults());
|
|
||||||
assertThat(events).contains("Events.PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldWidenInterfaceGettersToAllImplementations() throws IOException {
|
|
||||||
writeJava("com/example/Events.java", """
|
|
||||||
package com.example;
|
|
||||||
public enum Events { ALPHA, BETA }
|
|
||||||
""");
|
|
||||||
writeJava("com/example/AlphaPayload.java", """
|
|
||||||
package com.example;
|
|
||||||
public class AlphaPayload implements Payload {
|
|
||||||
public Events type() { return Events.ALPHA; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava("com/example/BetaPayload.java", """
|
|
||||||
package com.example;
|
|
||||||
public class BetaPayload implements Payload {
|
|
||||||
public Events type() { return Events.BETA; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava("com/example/Payload.java", """
|
|
||||||
package com.example;
|
|
||||||
public interface Payload {
|
|
||||||
Events type();
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
scanWorkspace();
|
|
||||||
List<String> resolved = accessorResolver.resolve(
|
|
||||||
"com.example.Payload",
|
|
||||||
"type",
|
|
||||||
new AccessorResolver.ReceiverContext(
|
|
||||||
context.getTypeDeclaration("com.example.Payload"),
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
null),
|
|
||||||
ResolutionBudget.defaults());
|
|
||||||
assertThat(resolved).containsExactlyInAnyOrder("Events.ALPHA", "Events.BETA");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class ExpressionAccessClassifierSuffixTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldTreatRecordStyleSuffixesAsTraceable() {
|
|
||||||
assertThat(expressionAccessClassifier.isTraceableAccessorSuffix(".getEvent()")).isTrue();
|
|
||||||
assertThat(expressionAccessClassifier.isTraceableAccessorSuffix(".type()")).isTrue();
|
|
||||||
assertThat(expressionAccessClassifier.isTraceableAccessorSuffix(".event()")).isTrue();
|
|
||||||
assertThat(expressionAccessClassifier.isTraceableAccessorSuffix(".hashCode()")).isFalse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class ConstructorAnalyzerGlobalScanTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldSkipGlobalScanByDefault() throws IOException {
|
|
||||||
writeJava("com/example/Unrelated.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Unrelated {
|
|
||||||
private String sharedName = "SHOULD_NOT_MATCH";
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava("com/example/Target.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Target {
|
|
||||||
private String sharedName;
|
|
||||||
public Target() { this.sharedName = "LOCAL"; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
scanWorkspace();
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration("com.example.Target");
|
|
||||||
List<String> traced = constructorAnalyzer.traceFieldInConstructors(
|
|
||||||
td, "sharedName", context, new HashSet<>(), ResolutionBudget.defaults());
|
|
||||||
assertThat(traced).contains("LOCAL");
|
|
||||||
assertThat(traced).doesNotContain("SHOULD_NOT_MATCH");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldAllowGlobalScanWhenExplicitlyEnabled() throws IOException {
|
|
||||||
writeJava("com/example/OnlyFieldOwner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class OnlyFieldOwner {
|
|
||||||
private String orphanField = "GLOBAL";
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava("com/example/Shell.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Shell {
|
|
||||||
private String orphanField;
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
scanWorkspace();
|
|
||||||
TypeDeclaration shell = context.getTypeDeclaration("com.example.Shell");
|
|
||||||
List<String> withoutGlobal = constructorAnalyzer.traceFieldInConstructors(
|
|
||||||
shell, "orphanField", context, new HashSet<>(), ResolutionBudget.defaults());
|
|
||||||
assertThat(withoutGlobal).isEmpty();
|
|
||||||
|
|
||||||
List<String> withGlobal = constructorAnalyzer.traceFieldInConstructors(
|
|
||||||
shell, "orphanField", context, new HashSet<>(), ResolutionBudget.withGlobalFieldScan());
|
|
||||||
assertThat(withGlobal).contains("GLOBAL");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class ConstantResolverSharingTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldUseSingleConstantResolverFromContext() {
|
|
||||||
assertThat(new HeuristicCallGraphEngine(context).getClass()).isNotNull();
|
|
||||||
ConstantResolver shared = context.getConstantResolver();
|
|
||||||
ConstantExtractor extractor = new ConstantExtractor(context, shared, mi -> null);
|
|
||||||
assertThat(extractor).isNotNull();
|
|
||||||
assertThat(shared).isSameAs(context.getConstantResolver());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class BooleanConstraintEvaluatorBindingsTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldAcceptMatchingEnumSwitchConstraint() {
|
|
||||||
assertThat(BooleanConstraintEvaluator.isCompatibleWithBindings(
|
|
||||||
"command == ORDER_PAY",
|
|
||||||
Map.of("command", "DomainCommand.ORDER_PAY"))).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRejectMismatchedEnumSwitchConstraint() {
|
|
||||||
assertThat(BooleanConstraintEvaluator.isCompatibleWithBindings(
|
|
||||||
"command == ORDER_SHIP",
|
|
||||||
Map.of("command", "DomainCommand.ORDER_PAY"))).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldAcceptMatchingStringSwitchConstraint() {
|
|
||||||
assertThat(BooleanConstraintEvaluator.isCompatibleWithBindings(
|
|
||||||
"commandKey == \"order.pay\"",
|
|
||||||
Map.of("commandKey", "order.pay"))).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRejectConstraintWhenBindingIsMissing() {
|
|
||||||
assertThat(BooleanConstraintEvaluator.isCompatibleWithBindings(
|
|
||||||
"command == ORDER_PAY",
|
|
||||||
Map.of("commandKey", "order.pay"))).isFalse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class BooleanConstraintEvaluatorTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldEvaluateLiteralEqualsAgainstMachineDomain() {
|
|
||||||
assertThat(BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
|
|
||||||
"\"ORDER\".equals(domain)", "OrderStateMachineConfiguration")).isTrue();
|
|
||||||
assertThat(BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
|
|
||||||
"\"PAYMENT\".equals(domain)", "OrderStateMachineConfiguration")).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldEvaluateReverseEqualsAndInequality() {
|
|
||||||
assertThat(BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
|
|
||||||
"domain.equals(\"ORDER\")", "OrderStateMachineConfiguration")).isTrue();
|
|
||||||
assertThat(BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
|
|
||||||
"domain != \"ORDER\"", "OrderStateMachineConfiguration")).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldEvaluateStringEqualsForKnownLiterals() {
|
|
||||||
assertThat(BooleanConstraintEvaluator.evaluateStringEquals("ORDER", "order", true)).isTrue();
|
|
||||||
assertThat(BooleanConstraintEvaluator.evaluateStringEquals("ORDER", "order", false)).isFalse();
|
|
||||||
assertThat(BooleanConstraintEvaluator.evaluateStringEquals(null, "ORDER", true)).isNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldEvaluateCombinedBooleanLogic() {
|
|
||||||
assertThat(BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
|
|
||||||
"\"ORDER\".equals(type) && \"ORDER\".equals(domain)", "OrderStateMachineConfiguration")).isTrue();
|
|
||||||
assertThat(BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
|
|
||||||
"\"ORDER\".equals(type) && \"PAYMENT\".equals(domain)", "OrderStateMachineConfiguration")).isFalse();
|
|
||||||
assertThat(BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
|
|
||||||
"\"PAYMENT\".equals(type) || \"ORDER\".equals(type)", "OrderStateMachineConfiguration")).isTrue();
|
|
||||||
assertThat(BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
|
|
||||||
"\"PAYMENT\".equals(type) || \"DOCUMENT\".equals(type)", "OrderStateMachineConfiguration")).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldEvaluateEqualsIgnoreCaseAgainstMachineDomain() {
|
|
||||||
assertThat(BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
|
|
||||||
"\"order\".equalsIgnoreCase(type)", "OrderStateMachineConfiguration")).isTrue();
|
|
||||||
assertThat(BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
|
|
||||||
"\"payment\".equalsIgnoreCase(type)", "OrderStateMachineConfiguration")).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldEvaluateNegatedDomainChecks() {
|
|
||||||
assertThat(BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
|
|
||||||
"!(\"PAYMENT\".equals(type))", "OrderStateMachineConfiguration")).isTrue();
|
|
||||||
assertThat(BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
|
|
||||||
"!(\"ORDER\".equals(type))", "OrderStateMachineConfiguration")).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void evaluateBooleanExpressionShouldHandleLogicalOperators() {
|
|
||||||
assertThat(BooleanConstraintEvaluator.evaluateBooleanExpression("true && false")).isFalse();
|
|
||||||
assertThat(BooleanConstraintEvaluator.evaluateBooleanExpression("true || false")).isTrue();
|
|
||||||
assertThat(BooleanConstraintEvaluator.evaluateBooleanExpression("!(false)")).isTrue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import org.eclipse.jdt.core.dom.*;
|
import org.eclipse.jdt.core.dom.*;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -9,8 +8,6 @@ import org.junit.jupiter.api.io.TempDir;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
@@ -527,166 +524,4 @@ public class ConstantResolverTest {
|
|||||||
String result2 = resolver.resolve(mi2, context);
|
String result2 = resolver.resolve(mi2, context);
|
||||||
assertThat(result2).isEqualTo("com.example.OrderEvent.SHIPPED");
|
assertThat(result2).isEqualTo("com.example.OrderEvent.SHIPPED");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveMapOfGetWithLiteralKey(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path dir = tempDir.resolve("com/example");
|
|
||||||
Files.createDirectories(dir);
|
|
||||||
Files.writeString(dir.resolve("Routes.java"),
|
|
||||||
"package com.example;\n" +
|
|
||||||
"public class Routes {\n" +
|
|
||||||
" public static final String PAY = \"OrderEvents.PAY\";\n" +
|
|
||||||
" public static final String SHIP = \"OrderEvents.SHIP\";\n" +
|
|
||||||
" public String lookup() {\n" +
|
|
||||||
" return java.util.Map.of(\"order.pay\", PAY, \"order.ship\", SHIP).get(\"order.pay\");\n" +
|
|
||||||
" }\n" +
|
|
||||||
"}");
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration("com.example.Routes");
|
|
||||||
MethodDeclaration lookup = td.getMethods()[0];
|
|
||||||
ReturnStatement rs = (ReturnStatement) lookup.getBody().statements().get(0);
|
|
||||||
|
|
||||||
ConstantResolver resolver = new ConstantResolver();
|
|
||||||
String result = resolver.resolve(rs.getExpression(), context);
|
|
||||||
|
|
||||||
assertThat(result).isEqualTo("OrderEvents.PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveStaticMapFieldGetWithLiteralKey(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path dir = tempDir.resolve("com/example");
|
|
||||||
Files.createDirectories(dir);
|
|
||||||
Files.writeString(dir.resolve("Dispatcher.java"),
|
|
||||||
"package com.example;\n" +
|
|
||||||
"import java.util.Map;\n" +
|
|
||||||
"public class Dispatcher {\n" +
|
|
||||||
" private static final Map<String, String> ROUTES = Map.of(\n" +
|
|
||||||
" \"order.pay\", \"OrderEvents.PAY\",\n" +
|
|
||||||
" \"order.ship\", \"OrderEvents.SHIP\"\n" +
|
|
||||||
" );\n" +
|
|
||||||
" public String route() {\n" +
|
|
||||||
" return ROUTES.get(\"order.ship\");\n" +
|
|
||||||
" }\n" +
|
|
||||||
"}");
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration("com.example.Dispatcher");
|
|
||||||
MethodDeclaration route = td.getMethods()[0];
|
|
||||||
ReturnStatement rs = (ReturnStatement) route.getBody().statements().get(0);
|
|
||||||
|
|
||||||
ConstantResolver resolver = new ConstantResolver();
|
|
||||||
String result = resolver.resolve(rs.getExpression(), context);
|
|
||||||
|
|
||||||
assertThat(result).isEqualTo("OrderEvents.SHIP");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveLiteralStringTransforms(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path dir = tempDir.resolve("com/example");
|
|
||||||
Files.createDirectories(dir);
|
|
||||||
Files.writeString(dir.resolve("OrderEvent.java"),
|
|
||||||
"package com.example;\n" +
|
|
||||||
"public enum OrderEvent { PAY, SHIP }\n");
|
|
||||||
Files.writeString(dir.resolve("Caller.java"),
|
|
||||||
"package com.example;\n" +
|
|
||||||
"public class Caller {\n" +
|
|
||||||
" public OrderEvent fromLiteral() {\n" +
|
|
||||||
" return OrderEvent.valueOf(\"pay\".toUpperCase());\n" +
|
|
||||||
" }\n" +
|
|
||||||
" public String trimmed() {\n" +
|
|
||||||
" return \" pay \".trim().toUpperCase();\n" +
|
|
||||||
" }\n" +
|
|
||||||
"}");
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration("com.example.Caller");
|
|
||||||
MethodDeclaration fromLiteral = td.getMethods()[0];
|
|
||||||
MethodDeclaration trimmed = td.getMethods()[1];
|
|
||||||
ReturnStatement rs1 = (ReturnStatement) fromLiteral.getBody().statements().get(0);
|
|
||||||
ReturnStatement rs2 = (ReturnStatement) trimmed.getBody().statements().get(0);
|
|
||||||
|
|
||||||
ConstantResolver resolver = new ConstantResolver();
|
|
||||||
|
|
||||||
assertThat(resolver.resolve(rs1.getExpression(), context)).isEqualTo("com.example.OrderEvent.PAY");
|
|
||||||
assertThat(resolver.resolve(rs2.getExpression(), context)).isEqualTo("PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveStaticFieldUsingCallPathScope(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path dir = tempDir.resolve("com/example");
|
|
||||||
Files.createDirectories(dir);
|
|
||||||
Files.writeString(dir.resolve("ApiController.java"),
|
|
||||||
"package com.example;\n" +
|
|
||||||
"import java.util.Map;\n" +
|
|
||||||
"public class ApiController {\n" +
|
|
||||||
" private static final Map<String, OrderEvent> ROUTES = Map.of(\n" +
|
|
||||||
" \"order.pay\", OrderEvent.PAY,\n" +
|
|
||||||
" \"order.ship\", OrderEvent.SHIP\n" +
|
|
||||||
" );\n" +
|
|
||||||
" public void dispatch(String commandKey) {\n" +
|
|
||||||
" OrderEvent event = ROUTES.get(commandKey);\n" +
|
|
||||||
" }\n" +
|
|
||||||
"}\n" +
|
|
||||||
"enum OrderEvent { PAY, SHIP }\n");
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
ASTParser parser = ASTParser.newParser(AST.JLS17);
|
|
||||||
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
|
||||||
parser.setSource(("class A { Object o = ROUTES.get(commandKey); }").toCharArray());
|
|
||||||
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
|
|
||||||
FieldDeclaration fd = (FieldDeclaration) ((TypeDeclaration) cu.types().get(0)).bodyDeclarations().get(0);
|
|
||||||
MethodInvocation getCall = (MethodInvocation) ((VariableDeclarationFragment) fd.fragments().get(0)).getInitializer();
|
|
||||||
|
|
||||||
ConstantExtractor extractor = new ConstantExtractor(context, new ConstantResolver(), mi -> null);
|
|
||||||
click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel.setCurrentPath(
|
|
||||||
List.of("com.example.ApiController.dispatch"));
|
|
||||||
try {
|
|
||||||
ConstantResolver resolver = new ConstantResolver();
|
|
||||||
SimpleName routesName = (SimpleName) getCall.getExpression();
|
|
||||||
assertThat(resolver.resolve(routesName, context))
|
|
||||||
.as("static ROUTES field should resolve to encoded map")
|
|
||||||
.startsWith("MAP:");
|
|
||||||
|
|
||||||
List<String> constants = new ArrayList<>();
|
|
||||||
extractor.extractConstantsFromExpression(getCall, constants);
|
|
||||||
assertThat(constants).contains("OrderEvent.PAY", "OrderEvent.SHIP");
|
|
||||||
} finally {
|
|
||||||
click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel.clearCurrentPath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveArrayIndexWithLiteralMath(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path dir = tempDir.resolve("com/example");
|
|
||||||
Files.createDirectories(dir);
|
|
||||||
Files.writeString(dir.resolve("Table.java"),
|
|
||||||
"package com.example;\n" +
|
|
||||||
"public class Table {\n" +
|
|
||||||
" private static final String[] EVENTS = {\"OrderEvents.PAY\", \"OrderEvents.SHIP\", \"OrderEvents.CANCEL\"};\n" +
|
|
||||||
" public String pick() {\n" +
|
|
||||||
" return EVENTS[1 + 1];\n" +
|
|
||||||
" }\n" +
|
|
||||||
"}");
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration("com.example.Table");
|
|
||||||
MethodDeclaration pick = td.getMethods()[0];
|
|
||||||
ReturnStatement rs = (ReturnStatement) pick.getBody().statements().get(0);
|
|
||||||
|
|
||||||
ConstantResolver resolver = new ConstantResolver();
|
|
||||||
String result = resolver.resolve(rs.getExpression(), context);
|
|
||||||
|
|
||||||
assertThat(result).isEqualTo("OrderEvents.CANCEL");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class EnterpriseStringMachineTypeTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveStringTypesForEnterpriseConfig() throws Exception {
|
|
||||||
Path projectRoot = Path.of("../state_machines/enterprise_order_system").toAbsolutePath().normalize();
|
|
||||||
Path javaRoot = projectRoot.resolve("src/main/java");
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.setSourcepath(List.of(javaRoot.toString()));
|
|
||||||
context.scan(javaRoot);
|
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
|
|
||||||
"click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig", context);
|
|
||||||
|
|
||||||
assertThat(types.stateTypeFqn()).isEqualTo("String");
|
|
||||||
assertThat(types.eventTypeFqn()).isEqualTo("String");
|
|
||||||
assertThat(MachineEnumCanonicalizer.canonicalizeLabel("NEW", types.stateTypeFqn()))
|
|
||||||
.isEqualTo("String.NEW");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.junit.jupiter.api.Nested;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class GeneratedSourceDiscoveryTest {
|
|
||||||
|
|
||||||
private final GeneratedSourceDiscovery discovery = new GeneratedSourceDiscovery();
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class MavenTarget {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDiscoverMapStructLikeSourcesUnderTarget(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path module = tempDir.resolve("service");
|
|
||||||
Path generated = module.resolve("target/generated-sources/annotations/com/acme/mapper");
|
|
||||||
Files.createDirectories(generated);
|
|
||||||
Files.writeString(generated.resolve("OrderMapperImpl.java"), """
|
|
||||||
package com.acme.mapper;
|
|
||||||
public class OrderMapperImpl implements OrderMapper {}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertThat(discovery.discoverSourceRoots(module))
|
|
||||||
.containsExactly(module.resolve("target").normalize());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldIgnoreCompiledClassesUnderTarget(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path module = tempDir.resolve("service");
|
|
||||||
Path classes = module.resolve("target/classes/com/acme");
|
|
||||||
Files.createDirectories(classes);
|
|
||||||
Files.writeString(classes.resolve("Accidental.java"), "class Accidental {}");
|
|
||||||
|
|
||||||
assertThat(discovery.discoverSourceRoots(module)).isEmpty();
|
|
||||||
assertThat(GeneratedSourceDiscovery.isExcludedBuildPath(module, classes.resolve("Accidental.java")))
|
|
||||||
.isTrue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class GradleBuild {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDiscoverAnnotationProcessorOutput(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path module = tempDir.resolve("app");
|
|
||||||
Path generated = module.resolve(
|
|
||||||
"build/generated/sources/annotationProcessor/java/main/com/acme/config");
|
|
||||||
Files.createDirectories(generated);
|
|
||||||
Files.writeString(generated.resolve("ApplicationConfigImpl.java"), """
|
|
||||||
package com.acme.config;
|
|
||||||
public class ApplicationConfigImpl {}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertThat(discovery.discoverSourceRoots(module))
|
|
||||||
.containsExactly(module.resolve("build").normalize());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldIgnoreGradleBuildClasses(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path module = tempDir.resolve("app");
|
|
||||||
Path classes = module.resolve("build/classes/java/main/com/acme");
|
|
||||||
Files.createDirectories(classes);
|
|
||||||
Files.writeString(classes.resolve("ShouldSkip.java"), "class ShouldSkip {}");
|
|
||||||
|
|
||||||
assertThat(discovery.discoverSourceRoots(module)).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldIgnoreGradleTestAnnotationProcessorOutput(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path module = tempDir.resolve("app");
|
|
||||||
Path generated = module.resolve(
|
|
||||||
"build/generated/sources/annotationProcessor/java/test/com/acme/testsupport");
|
|
||||||
Files.createDirectories(generated);
|
|
||||||
Files.writeString(generated.resolve("TestSupportImpl.java"), "class TestSupportImpl {}");
|
|
||||||
|
|
||||||
assertThat(discovery.discoverSourceRoots(module)).isEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class TestOutputExclusion {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldIgnoreMavenGeneratedTestSources(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path module = tempDir.resolve("service");
|
|
||||||
Path generated = module.resolve("target/generated-test-sources/test-annotations/com/acme");
|
|
||||||
Files.createDirectories(generated);
|
|
||||||
Files.writeString(generated.resolve("TestMapperImpl.java"), "class TestMapperImpl {}");
|
|
||||||
|
|
||||||
assertThat(discovery.discoverSourceRoots(module)).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldIgnoreTestClassesUnderTarget(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path module = tempDir.resolve("service");
|
|
||||||
Path testClasses = module.resolve("target/test-classes/com/acme");
|
|
||||||
Files.createDirectories(testClasses);
|
|
||||||
Files.writeString(testClasses.resolve("OrderServiceTest.java"), "class OrderServiceTest {}");
|
|
||||||
|
|
||||||
assertThat(discovery.discoverSourceRoots(module)).isEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class Integration {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldAddGeneratedRootsToProjectScanPaths(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path root = tempDir.resolve("monorepo");
|
|
||||||
Files.createDirectories(root.resolve(".git"));
|
|
||||||
Files.writeString(root.resolve("settings.gradle"), "include 'api'");
|
|
||||||
Path api = root.resolve("api");
|
|
||||||
Files.createDirectories(api);
|
|
||||||
Files.writeString(api.resolve("build.gradle"), "");
|
|
||||||
Path generated = api.resolve("target/generated-sources/annotations/com/example");
|
|
||||||
Files.createDirectories(generated);
|
|
||||||
Files.writeString(generated.resolve("GeneratedBean.java"), """
|
|
||||||
package com.example;
|
|
||||||
public class GeneratedBean {}
|
|
||||||
""");
|
|
||||||
|
|
||||||
StaticBuildFileAnalyzer analyzer = new StaticBuildFileAnalyzer();
|
|
||||||
ProjectModuleGraph graph = analyzer.analyze(root);
|
|
||||||
|
|
||||||
assertThat(graph.resolveScanPaths(root)).contains(api.resolve("target").normalize());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldSkipGeneratedRootsWhenDisabled(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path root = tempDir.resolve("monorepo");
|
|
||||||
Files.createDirectories(root.resolve(".git"));
|
|
||||||
Files.writeString(root.resolve("settings.gradle"), "include 'api'");
|
|
||||||
Path api = root.resolve("api");
|
|
||||||
Files.createDirectories(api);
|
|
||||||
Files.writeString(api.resolve("build.gradle"), "");
|
|
||||||
Path generated = api.resolve("target/generated-sources/annotations/com/example");
|
|
||||||
Files.createDirectories(generated);
|
|
||||||
Files.writeString(generated.resolve("GeneratedBean.java"), "package com.example; class GeneratedBean {}");
|
|
||||||
|
|
||||||
ProjectModuleGraph graph = new StaticBuildFileAnalyzer().analyze(root);
|
|
||||||
|
|
||||||
assertThat(graph.resolveScanPaths(root, false)).doesNotContain(api.resolve("target").normalize());
|
|
||||||
assertThat(graph.resolveScanPaths(root, true)).contains(api.resolve("target").normalize());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldIndexGeneratedTypesWhenScanningModule() throws IOException {
|
|
||||||
Path module = Files.createTempDirectory("sme-gen-src").resolve("service");
|
|
||||||
Path generated = module.resolve("target/generated-sources/annotations/com/acme");
|
|
||||||
Files.createDirectories(generated);
|
|
||||||
Files.writeString(generated.resolve("GeneratedEvent.java"), """
|
|
||||||
package com.acme;
|
|
||||||
public enum GeneratedEvent { PAY, SHIP }
|
|
||||||
""");
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(Set.of(module), Set.of());
|
|
||||||
|
|
||||||
assertThat(context.getCompilationUnits()).hasSize(1);
|
|
||||||
assertThat(context.getEnumValues("GeneratedEvent"))
|
|
||||||
.contains("com.acme.GeneratedEvent.PAY", "com.acme.GeneratedEvent.SHIP");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class MachineEnumCanonicalizerCrossPackageTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotRewriteCrossPackageEnumReferencesWhenSimpleNamesMatch() {
|
|
||||||
String machineEventType = "com.bar.OrderEvents";
|
|
||||||
String foreignEvent = "com.foo.OrderEvents.PAY";
|
|
||||||
|
|
||||||
assertThat(MachineEnumCanonicalizer.enumTypesMatch(machineEventType, "com.foo.OrderEvents"))
|
|
||||||
.isFalse();
|
|
||||||
assertThat(MachineEnumCanonicalizer.canonicalizeLabel(foreignEvent, machineEventType))
|
|
||||||
.isEqualTo(foreignEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldCanonicalizeImportStyleReferencesForMachineEnumType() {
|
|
||||||
assertThat(MachineEnumCanonicalizer.canonicalizeLabel(
|
|
||||||
"OrderEvents.PAY", "com.bar.OrderEvents"))
|
|
||||||
.isEqualTo("com.bar.OrderEvents.PAY");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Event;
|
|
||||||
import click.kamil.springstatemachineexporter.model.State;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class MachineEnumCanonicalizerTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveMachineEnumTypesFromConfigurerAdapter(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeSampleConfig(tempDir);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
|
|
||||||
"com.example.config.OrderStateMachineConfiguration", context);
|
|
||||||
|
|
||||||
assertThat(types.stateTypeFqn()).isEqualTo("com.example.order.OrderState");
|
|
||||||
assertThat(types.eventTypeFqn()).isEqualTo("com.example.order.OrderEvent");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldCanonicalizeTransitionIdentifiersUsingMachineTypes(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeSampleConfig(tempDir);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
|
|
||||||
"com.example.config.OrderStateMachineConfiguration", context);
|
|
||||||
|
|
||||||
Transition transition = new Transition();
|
|
||||||
transition.setEvent(Event.of("OrderEvent.PAY", "OrderEvent.PAY"));
|
|
||||||
transition.setSourceStates(List.of(State.of("OrderState.NEW", "OrderState.NEW")));
|
|
||||||
transition.setTargetStates(List.of(State.of("OrderState.PAID", "OrderState.PAID")));
|
|
||||||
|
|
||||||
MachineEnumCanonicalizer.canonicalizeTransitions(List.of(transition), types);
|
|
||||||
|
|
||||||
assertThat(transition.getEvent().fullIdentifier())
|
|
||||||
.isEqualTo("com.example.order.OrderEvent.PAY");
|
|
||||||
assertThat(transition.getEvent().rawName()).isEqualTo("OrderEvent.PAY");
|
|
||||||
assertThat(transition.getSourceStates().get(0).fullIdentifier())
|
|
||||||
.isEqualTo("com.example.order.OrderState.NEW");
|
|
||||||
assertThat(transition.getSourceStates().get(0).rawName()).isEqualTo("OrderState.NEW");
|
|
||||||
assertThat(transition.getTargetStates().get(0).fullIdentifier())
|
|
||||||
.isEqualTo("com.example.order.OrderState.PAID");
|
|
||||||
assertThat(transition.getTargetStates().get(0).rawName()).isEqualTo("OrderState.PAID");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldAlignBareRawNameToFnForm(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeSampleConfig(tempDir);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
|
|
||||||
"com.example.config.OrderStateMachineConfiguration", context);
|
|
||||||
|
|
||||||
Transition transition = new Transition();
|
|
||||||
transition.setEvent(Event.of("PAY", "PAY"));
|
|
||||||
transition.setSourceStates(List.of(State.of("NEW", "NEW")));
|
|
||||||
|
|
||||||
MachineEnumCanonicalizer.canonicalizeTransitions(List.of(transition), types);
|
|
||||||
|
|
||||||
assertThat(transition.getEvent().rawName()).isEqualTo("OrderEvent.PAY");
|
|
||||||
assertThat(transition.getSourceStates().get(0).rawName()).isEqualTo("OrderState.NEW");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldCanonicalizeBareConstantNames(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeSampleConfig(tempDir);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
|
|
||||||
"com.example.config.OrderStateMachineConfiguration", context);
|
|
||||||
|
|
||||||
assertThat(MachineEnumCanonicalizer.canonicalizeLabel("PAY", types.eventTypeFqn()))
|
|
||||||
.isEqualTo("com.example.order.OrderEvent.PAY");
|
|
||||||
assertThat(MachineEnumCanonicalizer.canonicalizeStateLabels(Set.of("NEW"), types.stateTypeFqn()))
|
|
||||||
.containsExactly("com.example.order.OrderState.NEW");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldLeaveStringStatesUntouched(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeSampleConfig(tempDir);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
|
|
||||||
"com.example.config.OrderStateMachineConfiguration", context);
|
|
||||||
|
|
||||||
assertThat(MachineEnumCanonicalizer.canonicalizeLabel("\"DRAFT\"", types.stateTypeFqn()))
|
|
||||||
.isEqualTo("DRAFT");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldCanonicalizePolymorphicEventsUsingMachineEventType(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeSampleConfig(tempDir);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
|
|
||||||
"com.example.config.OrderStateMachineConfiguration", context);
|
|
||||||
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder()
|
|
||||||
.event("OrderEvent.PAY")
|
|
||||||
.className("com.example.web.Controller")
|
|
||||||
.methodName("pay")
|
|
||||||
.sourceFile("Controller.java")
|
|
||||||
.polymorphicEvents(List.of("OrderEvent.PAY", "PAY"))
|
|
||||||
.eventTypeFqn("OrderEvent")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
TriggerPoint canonical = MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, types);
|
|
||||||
|
|
||||||
assertThat(canonical.getPolymorphicEvents()).containsExactly(
|
|
||||||
"com.example.order.OrderEvent.PAY",
|
|
||||||
"com.example.order.OrderEvent.PAY");
|
|
||||||
assertThat(canonical.getEvent()).isEqualTo("com.example.order.OrderEvent.PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldQualifyEventIdentifierToPackageFqn() {
|
|
||||||
String eventTypeFqn = "com.example.order.OrderEvent";
|
|
||||||
|
|
||||||
assertThat(MachineEnumCanonicalizer.qualifyEventIdentifier("PAY", eventTypeFqn))
|
|
||||||
.isEqualTo("com.example.order.OrderEvent.PAY");
|
|
||||||
assertThat(MachineEnumCanonicalizer.qualifyEventIdentifier("OrderEvent.PAY", eventTypeFqn))
|
|
||||||
.isEqualTo("com.example.order.OrderEvent.PAY");
|
|
||||||
assertThat(MachineEnumCanonicalizer.qualifyEventIdentifier("getType()", eventTypeFqn))
|
|
||||||
.isEqualTo("getType()");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeSampleConfig(Path tempDir) throws IOException {
|
|
||||||
Path orderPkg = tempDir.resolve("com/example/order");
|
|
||||||
Path configPkg = tempDir.resolve("com/example/config");
|
|
||||||
Files.createDirectories(orderPkg);
|
|
||||||
Files.createDirectories(configPkg);
|
|
||||||
|
|
||||||
Files.writeString(orderPkg.resolve("OrderState.java"),
|
|
||||||
"""
|
|
||||||
package com.example.order;
|
|
||||||
public enum OrderState { NEW, PAID }
|
|
||||||
""");
|
|
||||||
Files.writeString(orderPkg.resolve("OrderEvent.java"),
|
|
||||||
"""
|
|
||||||
package com.example.order;
|
|
||||||
public enum OrderEvent { PAY }
|
|
||||||
""");
|
|
||||||
Files.writeString(configPkg.resolve("OrderStateMachineConfiguration.java"),
|
|
||||||
"""
|
|
||||||
package com.example.config;
|
|
||||||
import com.example.order.OrderEvent;
|
|
||||||
import com.example.order.OrderState;
|
|
||||||
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
|
|
||||||
public class OrderStateMachineConfiguration
|
|
||||||
extends EnumStateMachineConfigurerAdapter<OrderState, OrderEvent> {
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class StateMachineTypeResolverEnterpriseTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveConcreteTypesThroughGenericAbstractBase() throws Exception {
|
|
||||||
Path root = findProjectRoot();
|
|
||||||
Path enterprise = root.resolve("state_machines/state_machine_enterprise");
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(enterprise);
|
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
|
|
||||||
"click.kamil.enterprise.machines.order.OrderStateMachineConfiguration", context);
|
|
||||||
|
|
||||||
assertThat(types.stateTypeFqn())
|
|
||||||
.isEqualTo("click.kamil.enterprise.machines.order.OrderState");
|
|
||||||
assertThat(types.eventTypeFqn())
|
|
||||||
.isEqualTo("click.kamil.enterprise.machines.order.OrderEvent");
|
|
||||||
assertThat(StateMachineTypeResolver.extendsEnumStateMachineConfigurer(
|
|
||||||
"click.kamil.enterprise.machines.order.OrderStateMachineConfiguration", context))
|
|
||||||
.isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Path findProjectRoot() {
|
|
||||||
Path current = Path.of(".").toAbsolutePath();
|
|
||||||
while (current != null && !Files.exists(current.resolve("settings.gradle"))) {
|
|
||||||
current = current.getParent();
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,510 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Nested;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class StaticBuildFileAnalyzerTest {
|
|
||||||
|
|
||||||
private final StaticBuildFileAnalyzer analyzer = new StaticBuildFileAnalyzer();
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class GradleIncludes {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldParseSettingsGradleIncludeList(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path root = tempDir.resolve("monorepo");
|
|
||||||
Files.createDirectories(root);
|
|
||||||
Files.writeString(root.resolve("settings.gradle"), """
|
|
||||||
rootProject.name = 'monorepo'
|
|
||||||
include 'module-a', 'module-b', 'module-c'
|
|
||||||
""");
|
|
||||||
createGradleModule(root, "module-a");
|
|
||||||
createGradleModule(root, "module-b");
|
|
||||||
createGradleModule(root, "module-c");
|
|
||||||
|
|
||||||
Map<String, Path> includes = analyzer.parseGradleIncludes(root);
|
|
||||||
|
|
||||||
assertThat(includes).hasSize(3);
|
|
||||||
assertThat(includes.get("module-a")).isEqualTo(root.resolve("module-a").normalize());
|
|
||||||
assertThat(includes.get("module-b")).isEqualTo(root.resolve("module-b").normalize());
|
|
||||||
assertThat(includes.get("module-c")).isEqualTo(root.resolve("module-c").normalize());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldParseSettingsGradleKtsIncludeCall(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path root = tempDir.resolve("monorepo");
|
|
||||||
Files.createDirectories(root);
|
|
||||||
Files.writeString(root.resolve("settings.gradle.kts"), """
|
|
||||||
include(":api", ":core")
|
|
||||||
""");
|
|
||||||
createGradleModule(root, "api");
|
|
||||||
createGradleModule(root, "core");
|
|
||||||
|
|
||||||
Map<String, Path> includes = analyzer.parseGradleIncludes(root);
|
|
||||||
|
|
||||||
assertThat(includes).containsKeys("api", "core");
|
|
||||||
assertThat(includes.get("api")).isEqualTo(root.resolve("api").normalize());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveNestedIncludePaths(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path root = tempDir.resolve("monorepo");
|
|
||||||
Path nested = root.resolve("state_machines/multi/api");
|
|
||||||
Files.createDirectories(nested);
|
|
||||||
Files.writeString(root.resolve("settings.gradle"), """
|
|
||||||
include ':state_machines:multi:api', ':state_machines:multi:core'
|
|
||||||
""");
|
|
||||||
createGradleModule(root, "state_machines/multi/api");
|
|
||||||
createGradleModule(root, "state_machines/multi/core");
|
|
||||||
|
|
||||||
Map<String, Path> includes = analyzer.parseGradleIncludes(root);
|
|
||||||
|
|
||||||
assertThat(includes.get("state_machines:multi:api"))
|
|
||||||
.isEqualTo(root.resolve("state_machines/multi/api").normalize());
|
|
||||||
assertThat(includes.get("state_machines:multi:core"))
|
|
||||||
.isEqualTo(root.resolve("state_machines/multi/core").normalize());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldHonorCustomProjectDirMapping(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path root = tempDir.resolve("monorepo");
|
|
||||||
Path custom = root.resolve("custom-location/service");
|
|
||||||
Files.createDirectories(custom);
|
|
||||||
Files.writeString(root.resolve("settings.gradle"), """
|
|
||||||
include ':service'
|
|
||||||
project(':service').projectDir = file('custom-location/service')
|
|
||||||
""");
|
|
||||||
createGradleModule(custom, ".");
|
|
||||||
|
|
||||||
Map<String, Path> includes = analyzer.parseGradleIncludes(root);
|
|
||||||
|
|
||||||
assertThat(includes.get("service")).isEqualTo(custom.normalize());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDiscoverAllIncludedModulesWhenScanningFromRoot(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path root = writeGradleMonorepo(tempDir);
|
|
||||||
|
|
||||||
ProjectModuleGraph graph = analyzer.analyze(root.resolve("module-a"));
|
|
||||||
Set<Path> scanPaths = graph.resolveScanPaths(root);
|
|
||||||
|
|
||||||
assertThat(scanPaths).containsExactlyInAnyOrder(
|
|
||||||
root.normalize(),
|
|
||||||
root.resolve("module-a").normalize(),
|
|
||||||
root.resolve("module-b").normalize(),
|
|
||||||
root.resolve("module-c").normalize());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class GradleDependencyGraph {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldBuildTransitiveProjectDependencyGraph(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path root = writeGradleMonorepo(tempDir);
|
|
||||||
|
|
||||||
ProjectModuleGraph graph = analyzer.analyze(root.resolve("module-a"));
|
|
||||||
|
|
||||||
List<ProjectModuleGraph.ModuleDependency> localDeps = graph.dependencies().stream()
|
|
||||||
.filter(dep -> dep.kind() == ProjectModuleGraph.DependencyKind.LOCAL_PROJECT)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
assertThat(localDeps).anyMatch(dep ->
|
|
||||||
dep.fromModuleId().contains("module-a") && dep.toModuleId().contains("module-b"));
|
|
||||||
assertThat(localDeps).anyMatch(dep ->
|
|
||||||
dep.fromModuleId().contains("module-b") && dep.toModuleId().contains("module-c"));
|
|
||||||
|
|
||||||
Set<Path> siblings = new SiblingDependencyResolver().resolveSiblings(graph, root.resolve("module-a"));
|
|
||||||
assertThat(siblings).containsExactlyInAnyOrder(
|
|
||||||
root.resolve("module-b").normalize(),
|
|
||||||
root.resolve("module-c").normalize());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldParseBuildGradleKtsProjectDependencies(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path root = tempDir.resolve("monorepo");
|
|
||||||
Files.createDirectories(root);
|
|
||||||
Files.createDirectories(root.resolve(".git"));
|
|
||||||
Files.writeString(root.resolve("settings.gradle.kts"), "include(\":app\", \":lib\")");
|
|
||||||
Files.createDirectories(root.resolve("app"));
|
|
||||||
Files.writeString(root.resolve("app/build.gradle.kts"), """
|
|
||||||
dependencies {
|
|
||||||
implementation(project(":lib"))
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
Files.createDirectories(root.resolve("lib"));
|
|
||||||
Files.writeString(root.resolve("lib/build.gradle.kts"), "");
|
|
||||||
|
|
||||||
ProjectModuleGraph graph = analyzer.analyze(root.resolve("app"));
|
|
||||||
boolean hasLibDep = graph.dependencies().stream()
|
|
||||||
.anyMatch(dep -> dep.kind() == ProjectModuleGraph.DependencyKind.LOCAL_PROJECT
|
|
||||||
&& dep.fromModuleId().contains("app")
|
|
||||||
&& dep.toModuleId().contains("lib"));
|
|
||||||
assertThat(hasLibDep).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldIgnoreTestImplementationProjectDependencies(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path root = tempDir.resolve("monorepo");
|
|
||||||
Files.createDirectories(root.resolve(".git"));
|
|
||||||
Files.writeString(root.resolve("settings.gradle"), "include 'app', 'lib', 'test-lib'");
|
|
||||||
createGradleModule(root, "app");
|
|
||||||
createGradleModule(root, "lib");
|
|
||||||
createGradleModule(root, "test-lib");
|
|
||||||
Files.writeString(root.resolve("app/build.gradle"), """
|
|
||||||
dependencies {
|
|
||||||
implementation project(':lib')
|
|
||||||
testImplementation project(':test-lib')
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
ProjectModuleGraph graph = analyzer.analyze(root.resolve("app"));
|
|
||||||
List<ProjectModuleGraph.ModuleDependency> localDeps = graph.dependencies().stream()
|
|
||||||
.filter(dep -> dep.kind() == ProjectModuleGraph.DependencyKind.LOCAL_PROJECT)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
assertThat(localDeps).anyMatch(dep -> dep.toModuleId().contains("lib"));
|
|
||||||
assertThat(localDeps).noneMatch(dep -> dep.toModuleId().contains("test-lib"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class MavenCoordinates {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldIndexModulesByGroupIdAndArtifactId(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path root = writeMavenMultiModule(tempDir);
|
|
||||||
|
|
||||||
ProjectModuleGraph graph = analyzer.analyze(root.resolve("module-a"));
|
|
||||||
|
|
||||||
assertThat(graph.modulesByCoordinates()).containsKeys(
|
|
||||||
new ProjectModuleGraph.MavenCoordinates("com.acme", "module-a"),
|
|
||||||
new ProjectModuleGraph.MavenCoordinates("com.acme", "module-b"),
|
|
||||||
new ProjectModuleGraph.MavenCoordinates("com.acme", "module-c"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDistinguishSameArtifactIdUnderDifferentGroups(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path root = tempDir.resolve("repo");
|
|
||||||
Files.createDirectories(root.resolve(".git"));
|
|
||||||
writePom(root.resolve("pom.xml"), """
|
|
||||||
<project>
|
|
||||||
<groupId>com.repo</groupId>
|
|
||||||
<artifactId>parent</artifactId>
|
|
||||||
<packaging>pom</packaging>
|
|
||||||
<modules>
|
|
||||||
<module>team-a/api</module>
|
|
||||||
<module>team-b/api</module>
|
|
||||||
<module>consumer</module>
|
|
||||||
</modules>
|
|
||||||
</project>
|
|
||||||
""");
|
|
||||||
writePom(root.resolve("team-a/api/pom.xml"), """
|
|
||||||
<project>
|
|
||||||
<groupId>com.team.a</groupId>
|
|
||||||
<artifactId>api</artifactId>
|
|
||||||
</project>
|
|
||||||
""");
|
|
||||||
writePom(root.resolve("team-b/api/pom.xml"), """
|
|
||||||
<project>
|
|
||||||
<groupId>com.team.b</groupId>
|
|
||||||
<artifactId>api</artifactId>
|
|
||||||
</project>
|
|
||||||
""");
|
|
||||||
writePom(root.resolve("consumer/pom.xml"), """
|
|
||||||
<project>
|
|
||||||
<groupId>com.consumer</groupId>
|
|
||||||
<artifactId>consumer</artifactId>
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.team.a</groupId>
|
|
||||||
<artifactId>api</artifactId>
|
|
||||||
<version>1.0</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</project>
|
|
||||||
""");
|
|
||||||
|
|
||||||
ProjectModuleGraph graph = analyzer.analyze(root.resolve("consumer"));
|
|
||||||
|
|
||||||
List<ProjectModuleGraph.ModuleDependency> deps = graph.localDependenciesOf(
|
|
||||||
graph.modulesByCoordinates()
|
|
||||||
.get(new ProjectModuleGraph.MavenCoordinates("com.consumer", "consumer"))
|
|
||||||
.id());
|
|
||||||
|
|
||||||
assertThat(deps).hasSize(1);
|
|
||||||
assertThat(deps.get(0).toModuleId()).isEqualTo(
|
|
||||||
graph.modulesByCoordinates().get(new ProjectModuleGraph.MavenCoordinates("com.team.a", "api")).id());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldMarkExternalLibraryWhenCoordinatesAreNotInRepo(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path root = tempDir.resolve("repo");
|
|
||||||
Files.createDirectories(root.resolve(".git"));
|
|
||||||
writePom(root.resolve("service/pom.xml"), """
|
|
||||||
<project>
|
|
||||||
<groupId>com.acme</groupId>
|
|
||||||
<artifactId>service</artifactId>
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter</artifactId>
|
|
||||||
<version>3.2.0</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</project>
|
|
||||||
""");
|
|
||||||
|
|
||||||
ProjectModuleGraph graph = analyzer.analyze(root.resolve("service"));
|
|
||||||
String serviceId = graph.modulesByCoordinates()
|
|
||||||
.get(new ProjectModuleGraph.MavenCoordinates("com.acme", "service")).id();
|
|
||||||
|
|
||||||
assertThat(graph.dependencies()).anyMatch(dep ->
|
|
||||||
dep.fromModuleId().equals(serviceId)
|
|
||||||
&& dep.kind() == ProjectModuleGraph.DependencyKind.EXTERNAL_LIBRARY
|
|
||||||
&& dep.externalCoordinates().key().equals("org.springframework.boot:spring-boot-starter"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldIgnoreTestScopedMavenDependencies(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path root = tempDir.resolve("repo");
|
|
||||||
Files.createDirectories(root.resolve(".git"));
|
|
||||||
writePom(root.resolve("pom.xml"), """
|
|
||||||
<project>
|
|
||||||
<groupId>com.repo</groupId>
|
|
||||||
<artifactId>parent</artifactId>
|
|
||||||
<packaging>pom</packaging>
|
|
||||||
<modules><module>app</module><module>test-support</module></modules>
|
|
||||||
</project>
|
|
||||||
""");
|
|
||||||
writePom(root.resolve("app/pom.xml"), """
|
|
||||||
<project>
|
|
||||||
<groupId>com.repo</groupId>
|
|
||||||
<artifactId>app</artifactId>
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.repo</groupId>
|
|
||||||
<artifactId>test-support</artifactId>
|
|
||||||
<version>1.0</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</project>
|
|
||||||
""");
|
|
||||||
writePom(root.resolve("test-support/pom.xml"), """
|
|
||||||
<project><groupId>com.repo</groupId><artifactId>test-support</artifactId></project>
|
|
||||||
""");
|
|
||||||
|
|
||||||
ProjectModuleGraph graph = analyzer.analyze(root.resolve("app"));
|
|
||||||
String appId = graph.modulesByCoordinates()
|
|
||||||
.get(new ProjectModuleGraph.MavenCoordinates("com.repo", "app")).id();
|
|
||||||
|
|
||||||
assertThat(graph.localDependenciesOf(appId)).isEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class MavenModules {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDiscoverAggregatorModules(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path root = tempDir.resolve("aggregator");
|
|
||||||
Files.createDirectories(root.resolve(".git"));
|
|
||||||
writePom(root.resolve("pom.xml"), """
|
|
||||||
<project>
|
|
||||||
<groupId>com.acme</groupId>
|
|
||||||
<artifactId>root</artifactId>
|
|
||||||
<packaging>pom</packaging>
|
|
||||||
<modules>
|
|
||||||
<module>service-x</module>
|
|
||||||
<module>service-y</module>
|
|
||||||
</modules>
|
|
||||||
</project>
|
|
||||||
""");
|
|
||||||
writePom(root.resolve("service-x/pom.xml"), """
|
|
||||||
<project><groupId>com.acme</groupId><artifactId>service-x</artifactId></project>
|
|
||||||
""");
|
|
||||||
writePom(root.resolve("service-y/pom.xml"), """
|
|
||||||
<project><groupId>com.acme</groupId><artifactId>service-y</artifactId></project>
|
|
||||||
""");
|
|
||||||
|
|
||||||
ProjectModuleGraph graph = analyzer.analyze(root);
|
|
||||||
Set<Path> siblings = new SiblingDependencyResolver().resolveSiblings(graph, root);
|
|
||||||
|
|
||||||
assertThat(siblings).containsExactlyInAnyOrder(
|
|
||||||
root.resolve("service-x").normalize(),
|
|
||||||
root.resolve("service-y").normalize());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveTransitiveMavenModuleDependencies(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path root = writeMavenMultiModule(tempDir);
|
|
||||||
|
|
||||||
Set<Path> siblings = new SiblingDependencyResolver().resolveSiblings(root.resolve("module-a"));
|
|
||||||
|
|
||||||
assertThat(siblings).containsExactlyInAnyOrder(
|
|
||||||
root.resolve("module-b").normalize(),
|
|
||||||
root.resolve("module-c").normalize(),
|
|
||||||
root.normalize());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveParentViaRelativePath(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path root = tempDir.resolve("root");
|
|
||||||
Path child = tempDir.resolve("child");
|
|
||||||
Files.createDirectories(root);
|
|
||||||
Files.createDirectories(child);
|
|
||||||
writePom(root.resolve("pom.xml"), """
|
|
||||||
<project><groupId>com.acme</groupId><artifactId>root</artifactId></project>
|
|
||||||
""");
|
|
||||||
writePom(child.resolve("pom.xml"), """
|
|
||||||
<project>
|
|
||||||
<parent>
|
|
||||||
<relativePath>../root/pom.xml</relativePath>
|
|
||||||
</parent>
|
|
||||||
<artifactId>child</artifactId>
|
|
||||||
</project>
|
|
||||||
""");
|
|
||||||
|
|
||||||
Set<Path> scanPaths = analyzer.analyze(child).resolveScanPaths(child);
|
|
||||||
|
|
||||||
assertThat(scanPaths).contains(root.toAbsolutePath().normalize());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldPreferNestedMavenAggregatorOverOuterGradleRoot(@TempDir Path tempDir) throws IOException {
|
|
||||||
Path outerGradle = tempDir.resolve("outer");
|
|
||||||
Path nestedMaven = outerGradle.resolve("nested-maven");
|
|
||||||
Files.createDirectories(outerGradle.resolve(".git"));
|
|
||||||
Files.writeString(outerGradle.resolve("settings.gradle"), "rootProject.name = 'outer'");
|
|
||||||
writePom(nestedMaven.resolve("pom.xml"), """
|
|
||||||
<project>
|
|
||||||
<groupId>com.nested</groupId>
|
|
||||||
<artifactId>parent</artifactId>
|
|
||||||
<packaging>pom</packaging>
|
|
||||||
<modules><module>api</module></modules>
|
|
||||||
</project>
|
|
||||||
""");
|
|
||||||
writePom(nestedMaven.resolve("api/pom.xml"), """
|
|
||||||
<project><groupId>com.nested</groupId><artifactId>api</artifactId></project>
|
|
||||||
""");
|
|
||||||
|
|
||||||
ProjectModuleGraph graph = analyzer.analyze(nestedMaven.resolve("api"));
|
|
||||||
|
|
||||||
assertThat(graph.workspaceRoot()).isEqualTo(nestedMaven.normalize());
|
|
||||||
assertThat(graph.allModuleDirectories()).containsExactlyInAnyOrder(
|
|
||||||
nestedMaven.resolve("api").normalize(),
|
|
||||||
nestedMaven.normalize());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class ExtractHelpers {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldExtractGradleIncludeTokensFromMixedSyntax() {
|
|
||||||
String settings = """
|
|
||||||
pluginManagement {}
|
|
||||||
include ':api'
|
|
||||||
include(":core", ":infra")
|
|
||||||
include 'web', 'worker'
|
|
||||||
""";
|
|
||||||
|
|
||||||
assertThat(StaticBuildFileAnalyzer.extractGradleIncludePaths(settings))
|
|
||||||
.containsExactlyInAnyOrder("api", "core", "infra", "web", "worker");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldExtractOwnMavenCoordinatesSkippingParentSection() {
|
|
||||||
String pom = """
|
|
||||||
<project>
|
|
||||||
<parent>
|
|
||||||
<groupId>com.parent</groupId>
|
|
||||||
<artifactId>parent</artifactId>
|
|
||||||
</parent>
|
|
||||||
<artifactId>child</artifactId>
|
|
||||||
<groupId>com.child</groupId>
|
|
||||||
</project>
|
|
||||||
""";
|
|
||||||
|
|
||||||
assertThat(StaticBuildFileAnalyzer.extractOwnMavenCoordinates(pom))
|
|
||||||
.contains(new ProjectModuleGraph.MavenCoordinates("com.child", "child"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Path writeGradleMonorepo(Path tempDir) throws IOException {
|
|
||||||
Path root = tempDir.resolve("my-root-project");
|
|
||||||
Files.createDirectories(root);
|
|
||||||
Files.createDirectories(root.resolve(".git"));
|
|
||||||
Files.writeString(root.resolve("settings.gradle"), "include 'module-a', 'module-b', 'module-c'");
|
|
||||||
createGradleModule(root, "module-a");
|
|
||||||
createGradleModule(root, "module-b");
|
|
||||||
createGradleModule(root, "module-c");
|
|
||||||
Files.writeString(root.resolve("module-a/build.gradle"), "dependencies { implementation project(':module-b') }");
|
|
||||||
Files.writeString(root.resolve("module-b/build.gradle"), "dependencies { implementation project(':module-c') }");
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Path writeMavenMultiModule(Path tempDir) throws IOException {
|
|
||||||
Path root = tempDir.resolve("my-maven-root");
|
|
||||||
Files.createDirectories(root.resolve(".git"));
|
|
||||||
writePom(root.resolve("pom.xml"), """
|
|
||||||
<project><groupId>com.acme</groupId><artifactId>root</artifactId></project>
|
|
||||||
""");
|
|
||||||
writePom(root.resolve("module-a/pom.xml"), """
|
|
||||||
<project>
|
|
||||||
<groupId>com.acme</groupId>
|
|
||||||
<artifactId>module-a</artifactId>
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.acme</groupId>
|
|
||||||
<artifactId>module-b</artifactId>
|
|
||||||
<version>1.0</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</project>
|
|
||||||
""");
|
|
||||||
writePom(root.resolve("module-b/pom.xml"), """
|
|
||||||
<project>
|
|
||||||
<groupId>com.acme</groupId>
|
|
||||||
<artifactId>module-b</artifactId>
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.acme</groupId>
|
|
||||||
<artifactId>module-c</artifactId>
|
|
||||||
<version>1.0</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</project>
|
|
||||||
""");
|
|
||||||
writePom(root.resolve("module-c/pom.xml"), """
|
|
||||||
<project><groupId>com.acme</groupId><artifactId>module-c</artifactId></project>
|
|
||||||
""");
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void createGradleModule(Path root, String modulePath) throws IOException {
|
|
||||||
Path moduleDir = ".".equals(modulePath) ? root : root.resolve(modulePath);
|
|
||||||
Files.createDirectories(moduleDir);
|
|
||||||
Path buildFile = moduleDir.resolve("build.gradle");
|
|
||||||
if (!Files.exists(buildFile)) {
|
|
||||||
Files.writeString(buildFile, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writePom(Path pomFile, String content) throws IOException {
|
|
||||||
Files.createDirectories(pomFile.getParent());
|
|
||||||
Files.writeString(pomFile, content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,414 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.DataFlowModel;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel;
|
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
|
||||||
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
|
|
||||||
import org.junit.jupiter.api.Nested;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import static click.kamil.springstatemachineexporter.analysis.service.CentralDispatcherTestSupport.*;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Quirk-focused tests for accessor inlining edge cases: super dispatch, field-backed chains,
|
|
||||||
* inheritance, and look-alike patterns that must not be confused.
|
|
||||||
*/
|
|
||||||
class AccessorInliningEdgeCasesTest {
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class SuperMethodAccessorShortcut {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldReadFieldDeclaredOnGrandparent(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Grand.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Grand {
|
|
||||||
protected String token = "GRAND";
|
|
||||||
public String getToken() { return token; }
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Child.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Child extends Grand {
|
|
||||||
public String capture() {
|
|
||||||
return super.getToken();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public void run() {
|
|
||||||
Child child = new Child();
|
|
||||||
String value = child.capture();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertResolved(tempDir, "value", "GRAND");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotShortcutWhenSuperGetterIsNonTrivial(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Base.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Base {
|
|
||||||
protected String event = "IGNORED";
|
|
||||||
public String getEvent() {
|
|
||||||
return event == null ? "NONE" : event;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Child.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Child extends Base {
|
|
||||||
public String capture() {
|
|
||||||
return super.getEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
public void run() {
|
|
||||||
Child child = new Child();
|
|
||||||
String value = child.capture();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertThat(accessorLookup(tempDir, "com.example.Base", "getEvent")).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void callGraphShouldPreferSuperFieldOverChildOverride(@TempDir Path tempDir) throws Exception {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class ApiController {
|
|
||||||
ChildController controller;
|
|
||||||
public void run() {
|
|
||||||
controller.process();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class ChildController extends ParentController {
|
|
||||||
@Override
|
|
||||||
public OrderEvent getEvent() { return OrderEvent.SHIP; }
|
|
||||||
public void process() {
|
|
||||||
StateMachine sm = new StateMachine();
|
|
||||||
sm.sendEvent(super.getEvent());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class ParentController {
|
|
||||||
private OrderEvent event = OrderEvent.PAY;
|
|
||||||
public OrderEvent getEvent() { return event; }
|
|
||||||
}
|
|
||||||
enum OrderEvent { PAY, SHIP }
|
|
||||||
class StateMachine { public void sendEvent(OrderEvent e) {} }
|
|
||||||
class OrderStateMachineConfig {}
|
|
||||||
""";
|
|
||||||
CodebaseContext context = scanSource(source, tempDir);
|
|
||||||
CallChain chain = resolveChain(context, "com.example.ApiController", "run", "com.example.StateMachine");
|
|
||||||
assertPolyEvents(chain, "OrderEvent.PAY");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class FieldBackedGetterChains {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveDeepFieldBackedChainWithoutHopLimit(@TempDir Path tempDir) throws Exception {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class ApiController {
|
|
||||||
CentralDispatcher dispatcher;
|
|
||||||
public void pay(Order order) {
|
|
||||||
dispatcher.fire(order);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class CentralDispatcher {
|
|
||||||
public void fire(Order order) {
|
|
||||||
StateMachine sm = new StateMachine();
|
|
||||||
sm.sendEvent(order.getPayload().getEvent().getCode());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class Order {
|
|
||||||
private Payload payload = new Payload();
|
|
||||||
public Payload getPayload() { return payload; }
|
|
||||||
}
|
|
||||||
class Payload {
|
|
||||||
private Event event = new Event();
|
|
||||||
public Event getEvent() { return event; }
|
|
||||||
}
|
|
||||||
class Event {
|
|
||||||
private OrderEvent code = OrderEvent.PAY;
|
|
||||||
public OrderEvent getCode() { return code; }
|
|
||||||
}
|
|
||||||
enum OrderEvent { PAY, SHIP }
|
|
||||||
class StateMachine { public void sendEvent(OrderEvent e) {} }
|
|
||||||
class OrderStateMachineConfig {}
|
|
||||||
""";
|
|
||||||
CodebaseContext context = scanSource(source, tempDir);
|
|
||||||
CallChain chain = resolveChain(context, "com.example.ApiController", "pay", "com.example.StateMachine");
|
|
||||||
assertPolyEvents(chain, "OrderEvent.PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveFieldInitializerDeclaredOnSuperclass(@TempDir Path tempDir) throws Exception {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class ApiController {
|
|
||||||
CentralDispatcher dispatcher;
|
|
||||||
public void pay(BaseWrapper wrapper) {
|
|
||||||
dispatcher.fire(wrapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class CentralDispatcher {
|
|
||||||
public void fire(BaseWrapper wrapper) {
|
|
||||||
StateMachine sm = new StateMachine();
|
|
||||||
sm.sendEvent(wrapper.getPayload().getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class BaseWrapper {
|
|
||||||
protected Payload payload = new Payload();
|
|
||||||
public Payload getPayload() { return payload; }
|
|
||||||
}
|
|
||||||
class Payload {
|
|
||||||
private OrderEvent type = OrderEvent.PAY;
|
|
||||||
public OrderEvent getType() { return type; }
|
|
||||||
}
|
|
||||||
enum OrderEvent { PAY, SHIP }
|
|
||||||
class StateMachine { public void sendEvent(OrderEvent e) {} }
|
|
||||||
class OrderStateMachineConfig {}
|
|
||||||
""";
|
|
||||||
CodebaseContext context = scanSource(source, tempDir);
|
|
||||||
CallChain chain = resolveChain(context, "com.example.ApiController", "pay", "com.example.StateMachine");
|
|
||||||
assertPolyEvents(chain, "OrderEvent.PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotUseIndexedFieldWhenInnerGetterReturnsLiteral(@TempDir Path tempDir) throws Exception {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class ApiController {
|
|
||||||
CentralDispatcher dispatcher;
|
|
||||||
public void pay(EventWrapper wrapper) {
|
|
||||||
dispatcher.fire(wrapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class CentralDispatcher {
|
|
||||||
public void fire(EventWrapper wrapper) {
|
|
||||||
StateMachine sm = new StateMachine();
|
|
||||||
sm.sendEvent(wrapper.getEvent().getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class EventWrapper {
|
|
||||||
private RichEvent event = new RichEvent();
|
|
||||||
public RichEvent getEvent() { return event; }
|
|
||||||
}
|
|
||||||
class RichEvent {
|
|
||||||
private OrderEvent type = OrderEvent.PAY;
|
|
||||||
public OrderEvent getType() {
|
|
||||||
return OrderEvent.SHIP;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enum OrderEvent { PAY, SHIP }
|
|
||||||
class StateMachine { public void sendEvent(OrderEvent e) {} }
|
|
||||||
class OrderStateMachineConfig {}
|
|
||||||
""";
|
|
||||||
CodebaseContext context = scanSource(source, tempDir);
|
|
||||||
CallChain chain = resolveChain(context, "com.example.ApiController", "pay", "com.example.StateMachine");
|
|
||||||
assertPolyEvents(chain, "OrderEvent.SHIP");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDistinguishFieldBackedChainFromLiteralReturnChain(@TempDir Path tempDir) throws Exception {
|
|
||||||
String literalSource = """
|
|
||||||
package com.example;
|
|
||||||
public class ApiController {
|
|
||||||
CentralDispatcher dispatcher;
|
|
||||||
public void pay(EventWrapper wrapper) {
|
|
||||||
dispatcher.fire(wrapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class CentralDispatcher {
|
|
||||||
public void fire(EventWrapper wrapper) {
|
|
||||||
StateMachine sm = new StateMachine();
|
|
||||||
sm.sendEvent(wrapper.getEvent().getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class EventWrapper {
|
|
||||||
public RichEvent getEvent() { return new RichEvent(); }
|
|
||||||
}
|
|
||||||
class RichEvent {
|
|
||||||
public OrderEvent getType() { return OrderEvent.PAY; }
|
|
||||||
}
|
|
||||||
enum OrderEvent { PAY, SHIP }
|
|
||||||
class StateMachine { public void sendEvent(OrderEvent e) {} }
|
|
||||||
class OrderStateMachineConfig {}
|
|
||||||
""";
|
|
||||||
Path literalDir = tempDir.resolve("literal");
|
|
||||||
Files.createDirectories(literalDir);
|
|
||||||
CodebaseContext literalContext = scanSource(literalSource, literalDir);
|
|
||||||
CallChain literalChain = resolveChain(
|
|
||||||
literalContext, "com.example.ApiController", "pay", "com.example.StateMachine");
|
|
||||||
assertPolyEvents(literalChain, "OrderEvent.PAY");
|
|
||||||
|
|
||||||
String fieldSource = """
|
|
||||||
package com.example;
|
|
||||||
public class ApiController {
|
|
||||||
CentralDispatcher dispatcher;
|
|
||||||
public void pay(EventWrapper wrapper) {
|
|
||||||
dispatcher.fire(wrapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class CentralDispatcher {
|
|
||||||
public void fire(EventWrapper wrapper) {
|
|
||||||
StateMachine sm = new StateMachine();
|
|
||||||
sm.sendEvent(wrapper.getEvent().getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class EventWrapper {
|
|
||||||
private RichEvent event = new RichEvent();
|
|
||||||
public RichEvent getEvent() { return event; }
|
|
||||||
}
|
|
||||||
class RichEvent {
|
|
||||||
private OrderEvent type = OrderEvent.PAY;
|
|
||||||
public OrderEvent getType() { return type; }
|
|
||||||
}
|
|
||||||
enum OrderEvent { PAY, SHIP }
|
|
||||||
class StateMachine { public void sendEvent(OrderEvent e) {} }
|
|
||||||
class OrderStateMachineConfig {}
|
|
||||||
""";
|
|
||||||
Path fieldDir = tempDir.resolve("field");
|
|
||||||
Files.createDirectories(fieldDir);
|
|
||||||
CodebaseContext fieldContext = scanSource(fieldSource, fieldDir);
|
|
||||||
CallChain fieldChain = resolveChain(
|
|
||||||
fieldContext, "com.example.ApiController", "pay", "com.example.StateMachine");
|
|
||||||
assertPolyEvents(fieldChain, "OrderEvent.PAY");
|
|
||||||
assertLinkedEvent(
|
|
||||||
linkEndpointToTransition(fieldChain, "com.example.OrderStateMachineConfig",
|
|
||||||
transition("NEW", "PAID", "OrderEvent.PAY"),
|
|
||||||
transition("PAID", "SHIPPED", "OrderEvent.SHIP")),
|
|
||||||
"OrderEvent.PAY");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class LookAlikePatterns {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotTreatListGetAsBeanAccessorInDataflow(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
import java.util.List;
|
|
||||||
public class Runner {
|
|
||||||
public void run() {
|
|
||||||
List<String> events = List.of("A", "B");
|
|
||||||
String value = events.get(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
CodebaseContext context = scan(tempDir);
|
|
||||||
assertThat(context.getAccessorIndex().lookup("java.util.List", "get")).isEmpty();
|
|
||||||
DataFlowModel model = new JdtDataFlowModel(context);
|
|
||||||
Expression initializer = findInitializer(context, "run", "value");
|
|
||||||
assertThat(model.getReachingDefinitions(initializer)).isNotEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotIndexBareGetMethod(@TempDir Path tempDir) throws IOException {
|
|
||||||
writeJava(tempDir, "com/example/Runner.java", """
|
|
||||||
package com.example;
|
|
||||||
public class Runner {
|
|
||||||
static class Box {
|
|
||||||
public String get() { return "X"; }
|
|
||||||
}
|
|
||||||
public void run() {
|
|
||||||
Box box = new Box();
|
|
||||||
String value = box.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
assertThat(accessorLookup(tempDir, "com.example.Runner.Box", "get")).isEmpty();
|
|
||||||
assertResolved(tempDir, "value", "X");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void assertResolved(Path tempDir, String variableName, String expected) throws IOException {
|
|
||||||
CodebaseContext context = scan(tempDir);
|
|
||||||
DataFlowModel model = new JdtDataFlowModel(context);
|
|
||||||
Expression initializer = findInitializer(context, "run", variableName);
|
|
||||||
String resolved = model.resolveValue(initializer, context);
|
|
||||||
assertThat(resolved).isEqualTo(expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Optional<AccessorSummary> accessorLookup(Path tempDir, String ownerFqn, String methodName)
|
|
||||||
throws IOException {
|
|
||||||
return scan(tempDir).getAccessorIndex().lookup(ownerFqn, methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CodebaseContext scan(Path tempDir) throws IOException {
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.setResolveBindings(true);
|
|
||||||
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
|
||||||
context.scan(tempDir);
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Expression findInitializer(CodebaseContext context, String methodName, String variableName) {
|
|
||||||
for (TypeDeclaration type : context.getTypeDeclarations()) {
|
|
||||||
Expression result = findInitializerInType(type, methodName, variableName);
|
|
||||||
if (result != null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new AssertionError("Initializer not found for " + variableName + " in " + methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Expression findInitializerInType(TypeDeclaration type, String methodName, String variableName) {
|
|
||||||
for (MethodDeclaration method : type.getMethods()) {
|
|
||||||
if (!method.getName().getIdentifier().equals(methodName) || method.getBody() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (Object statementObj : method.getBody().statements()) {
|
|
||||||
if (statementObj instanceof VariableDeclarationStatement statement) {
|
|
||||||
for (Object fragmentObj : statement.fragments()) {
|
|
||||||
if (fragmentObj instanceof VariableDeclarationFragment fragment
|
|
||||||
&& fragment.getName().getIdentifier().equals(variableName)) {
|
|
||||||
return fragment.getInitializer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (TypeDeclaration nested : type.getTypes()) {
|
|
||||||
Expression result = findInitializerInType(nested, methodName, variableName);
|
|
||||||
if (result != null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeJava(Path tempDir, String relativePath, String source) throws IOException {
|
|
||||||
Path file = tempDir.resolve(relativePath);
|
|
||||||
Files.createDirectories(file.getParent());
|
|
||||||
Files.writeString(file, source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,367 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.CallChainEnricher;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.EntryPointEnricher;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.app.StateMachineAggregator;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Guards the shared analysis caches used when one codebase produces many inherited state machines.
|
|
||||||
*
|
|
||||||
* <p>Typical repo shape: one abstract config, several abstract branches, many concrete configs.
|
|
||||||
* The exporter scans once, caches the call graph, then enriches each machine from the same context.
|
|
||||||
*/
|
|
||||||
class AnalysisCacheCorrectnessTest {
|
|
||||||
|
|
||||||
private static Path findProjectRoot() {
|
|
||||||
Path current = Path.of(".").toAbsolutePath();
|
|
||||||
while (current != null && !Files.exists(current.resolve("settings.gradle"))) {
|
|
||||||
current = current.getParent();
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The call graph is expensive to build; it must be stored on {@link CodebaseContext} and reused
|
|
||||||
* by subsequent engines pointing at the same scan result.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void heuristicCallGraphShouldBeCachedOnContext(@TempDir Path tempDir) throws Exception {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
class A { void go() { new B().run("PAY"); } }
|
|
||||||
class B { void run(String s) { new C().fire(com.example.E.PAY); } }
|
|
||||||
enum E { PAY, SHIP }
|
|
||||||
class C { void fire(E e) {} }
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("App.java"), source);
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
HeuristicCallGraphEngine firstEngine = new HeuristicCallGraphEngine(context);
|
|
||||||
firstEngine.findChains(
|
|
||||||
List.of(EntryPoint.builder().className("com.example.A").methodName("go").build()),
|
|
||||||
List.of(TriggerPoint.builder().className("com.example.C").methodName("fire").event("e").build()));
|
|
||||||
|
|
||||||
assertThat(context.getCache()).containsKey("heuristicCallGraph");
|
|
||||||
Object cachedGraph = context.getCache().get("heuristicCallGraph");
|
|
||||||
|
|
||||||
HeuristicCallGraphEngine secondEngine = new HeuristicCallGraphEngine(context);
|
|
||||||
secondEngine.findChains(
|
|
||||||
List.of(EntryPoint.builder().className("com.example.A").methodName("go").build()),
|
|
||||||
List.of(TriggerPoint.builder().className("com.example.C").methodName("fire").event("e").build()));
|
|
||||||
|
|
||||||
assertThat(context.getCache().get("heuristicCallGraph")).isSameAs(cachedGraph);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void jdtIntelligenceProviderShouldCacheCallChains(@TempDir Path tempDir) throws Exception {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
class A { void go() { new B().run("PAY"); } }
|
|
||||||
class B { void run(String s) { new C().fire(com.example.E.PAY); } }
|
|
||||||
enum E { PAY, SHIP }
|
|
||||||
class C { void fire(E e) {} }
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("App.java"), source);
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.setResolveBindings(true);
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
JdtIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, tempDir);
|
|
||||||
EntryPoint entry = EntryPoint.builder().className("com.example.A").methodName("go").build();
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.C").methodName("fire").event("e").build();
|
|
||||||
List<EntryPoint> entryPoints = List.of(entry);
|
|
||||||
List<TriggerPoint> triggers = List.of(trigger);
|
|
||||||
|
|
||||||
List<CallChain> first = intelligence.findCallChains(entryPoints, triggers);
|
|
||||||
List<CallChain> second = intelligence.findCallChains(entryPoints, triggers);
|
|
||||||
|
|
||||||
assertThat(second).isSameAs(first);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Two consecutive {@link CallGraphEngine#findChains} calls on a cached graph must agree.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void repeatedCallChainResolutionShouldBeDeterministic(@TempDir Path tempDir) throws Exception {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
class Controller {
|
|
||||||
Dispatcher dispatcher;
|
|
||||||
public void pay() { dispatcher.dispatch("ORDER", "PAY"); }
|
|
||||||
}
|
|
||||||
class Dispatcher {
|
|
||||||
public void dispatch(String type, String action) {
|
|
||||||
StateMachine sm = new StateMachine();
|
|
||||||
if ("ORDER".equals(type)) {
|
|
||||||
OrderEvent ev = OrderEvent.valueOf(action);
|
|
||||||
sm.sendEvent(ev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enum OrderEvent { PAY, SHIP }
|
|
||||||
class StateMachine { void sendEvent(OrderEvent e) {} }
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("App.java"), source);
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
EntryPoint pay = EntryPoint.builder().className("com.example.Controller").methodName("pay").build();
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("e").build();
|
|
||||||
|
|
||||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
|
||||||
CallChain first = engine.findChains(List.of(pay), List.of(trigger)).get(0);
|
|
||||||
CallChain second = engine.findChains(List.of(pay), List.of(trigger)).get(0);
|
|
||||||
|
|
||||||
assertThat(second.getTriggerPoint().getPolymorphicEvents())
|
|
||||||
.isEqualTo(first.getTriggerPoint().getPolymorphicEvents());
|
|
||||||
assertThat(second.getMethodChain()).isEqualTo(first.getMethodChain());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A single {@link TransitionLinkerEnricher} instance is reused across machines during export.
|
|
||||||
* Its internal memoization must not let machine B overwrite machine A's matched transitions.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void sharedTransitionLinkerShouldNotCrossContaminateMachineResults(@TempDir Path tempDir) throws Exception {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
class OrderController {
|
|
||||||
SharedBus bus;
|
|
||||||
void pay() { bus.orderPay(); }
|
|
||||||
}
|
|
||||||
class ShipmentController {
|
|
||||||
SharedBus bus;
|
|
||||||
void go() { bus.shipmentDispatch(); }
|
|
||||||
}
|
|
||||||
class SharedBus {
|
|
||||||
public void orderPay() {
|
|
||||||
OrderMachine sm = new OrderMachine();
|
|
||||||
sm.sendEvent(OrderEvent.PAY);
|
|
||||||
}
|
|
||||||
public void shipmentDispatch() {
|
|
||||||
ShipmentMachine sm = new ShipmentMachine();
|
|
||||||
sm.sendEvent(ShipmentEvent.DISPATCH);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enum OrderEvent { PAY }
|
|
||||||
enum ShipmentEvent { DISPATCH }
|
|
||||||
class OrderMachine { void sendEvent(OrderEvent e) {} }
|
|
||||||
class ShipmentMachine { void sendEvent(ShipmentEvent e) {} }
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("App.java"), source);
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
|
||||||
|
|
||||||
CallChain orderChain = engine.findChains(
|
|
||||||
List.of(EntryPoint.builder().className("com.example.OrderController").methodName("pay").build()),
|
|
||||||
List.of(TriggerPoint.builder().className("com.example.OrderMachine").methodName("sendEvent").event("e").build())
|
|
||||||
).get(0);
|
|
||||||
CallChain shipmentChain = engine.findChains(
|
|
||||||
List.of(EntryPoint.builder().className("com.example.ShipmentController").methodName("go").build()),
|
|
||||||
List.of(TriggerPoint.builder().className("com.example.ShipmentMachine").methodName("sendEvent").event("e").build())
|
|
||||||
).get(0);
|
|
||||||
|
|
||||||
TransitionLinkerEnricher sharedLinker = new TransitionLinkerEnricher();
|
|
||||||
|
|
||||||
AnalysisResult orderMachine = analysisWithChain(
|
|
||||||
"com.example.OrderStateMachineConfig",
|
|
||||||
orderChain,
|
|
||||||
CentralDispatcherTestSupport.transition("NEW", "PAID", "OrderEvent.PAY"));
|
|
||||||
AnalysisResult shipmentMachine = analysisWithChain(
|
|
||||||
"com.example.ShipmentStateMachineConfig",
|
|
||||||
shipmentChain,
|
|
||||||
CentralDispatcherTestSupport.transition("READY", "IN_TRANSIT", "ShipmentEvent.DISPATCH"));
|
|
||||||
|
|
||||||
sharedLinker.enrich(orderMachine, null, null);
|
|
||||||
sharedLinker.enrich(shipmentMachine, null, null);
|
|
||||||
|
|
||||||
assertThat(orderMachine.getMetadata().getCallChains().get(0).getMatchedTransitions())
|
|
||||||
.extracting("event")
|
|
||||||
.containsExactly("OrderEvent.PAY");
|
|
||||||
assertThat(shipmentMachine.getMetadata().getCallChains().get(0).getMatchedTransitions())
|
|
||||||
.extracting("event")
|
|
||||||
.containsExactly("ShipmentEvent.DISPATCH");
|
|
||||||
|
|
||||||
// Re-enrich order machine after shipment machine was processed; result must stay stable.
|
|
||||||
sharedLinker.enrich(orderMachine, null, null);
|
|
||||||
assertThat(orderMachine.getMetadata().getCallChains().get(0).getMatchedTransitions())
|
|
||||||
.extracting("event")
|
|
||||||
.containsExactly("OrderEvent.PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exercises a real inherited hierarchy (F1/F2/G1/G2) from one scan + one intelligence provider,
|
|
||||||
* mirroring repos where many concrete configs extend shared abstract layers.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void inheritedHierarchyShouldProduceStableDistinctMachineSnapshots() throws Exception {
|
|
||||||
Path sampleRoot = findProjectRoot().resolve("state_machines/inheritance_extra_functions3_state_machine");
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.setProjectRoot(sampleRoot);
|
|
||||||
context.setSourcepath(List.of(sampleRoot.resolve("src/main/java").toString()));
|
|
||||||
context.setResolveBindings(true);
|
|
||||||
context.scan(sampleRoot);
|
|
||||||
|
|
||||||
JdtIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, sampleRoot);
|
|
||||||
EnrichmentService enrichmentService = new EnrichmentService(List.of(
|
|
||||||
new TriggerEnricher(),
|
|
||||||
new EntryPointEnricher(),
|
|
||||||
new CallChainEnricher(),
|
|
||||||
new click.kamil.springstatemachineexporter.analysis.enricher.TriggerCanonicalizationEnricher(),
|
|
||||||
new TransitionLinkerEnricher()));
|
|
||||||
|
|
||||||
List<String> configNames = List.of(
|
|
||||||
"F1StateMachineConfiguration",
|
|
||||||
"F2StateMachineConfiguration",
|
|
||||||
"G1StateMachineConfiguration",
|
|
||||||
"G2StateMachineConfiguration");
|
|
||||||
|
|
||||||
Map<String, Snapshot> firstPass = enrichAllConfigs(context, intelligence, enrichmentService, configNames);
|
|
||||||
Map<String, Snapshot> secondPass = enrichAllConfigs(context, intelligence, enrichmentService, configNames);
|
|
||||||
|
|
||||||
assertThat(secondPass).isEqualTo(firstPass);
|
|
||||||
assertThat(firstPass).hasSize(4);
|
|
||||||
|
|
||||||
List<Snapshot> snapshots = new ArrayList<>(firstPass.values());
|
|
||||||
assertThat(snapshots.get(0).transitionCount()).isNotEqualTo(snapshots.get(2).transitionCount());
|
|
||||||
assertThat(snapshots.get(0).eventNames()).isNotEqualTo(snapshots.get(2).eventNames());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synthetic repo shape: 1 root abstract → 3 branch abstracts → 4 concrete configs each (12 machines).
|
|
||||||
* One scan and one intelligence provider must yield stable, distinct snapshots on repeat enrichment.
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* RootAbstractSmConfig
|
|
||||||
* ├── BranchAAbstractSmConfig → BranchA1..A4SmConfig
|
|
||||||
* ├── BranchBAbstractSmConfig → BranchB1..B4SmConfig
|
|
||||||
* └── BranchCAbstractSmConfig → BranchC1..C4SmConfig
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void twelveConfigSyntheticHierarchyShouldProduceStableDistinctSnapshots(@TempDir Path tempDir) throws Exception {
|
|
||||||
TwelveConfigHierarchyTestSupport.writeSources(tempDir);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.setProjectRoot(tempDir);
|
|
||||||
context.setResolveBindings(true);
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
JdtIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, tempDir);
|
|
||||||
EnrichmentService enrichmentService = new EnrichmentService(List.of(
|
|
||||||
new TriggerEnricher(),
|
|
||||||
new EntryPointEnricher(),
|
|
||||||
new CallChainEnricher(),
|
|
||||||
new click.kamil.springstatemachineexporter.analysis.enricher.TriggerCanonicalizationEnricher(),
|
|
||||||
new TransitionLinkerEnricher()));
|
|
||||||
|
|
||||||
List<String> configNames = TwelveConfigHierarchyTestSupport.concreteConfigNames();
|
|
||||||
|
|
||||||
Map<String, Snapshot> firstPass = enrichAllConfigs(context, intelligence, enrichmentService, configNames);
|
|
||||||
Object cachedGraphAfterFirstPass = context.getCache().get("jdtCallGraph");
|
|
||||||
|
|
||||||
Map<String, Snapshot> secondPass = enrichAllConfigs(context, intelligence, enrichmentService, configNames);
|
|
||||||
|
|
||||||
assertThat(firstPass).hasSize(12);
|
|
||||||
assertThat(secondPass).isEqualTo(firstPass);
|
|
||||||
assertThat(new ArrayList<>(firstPass.values())).doesNotHaveDuplicates();
|
|
||||||
|
|
||||||
// Each concrete config inherits root + branch + its own transition.
|
|
||||||
assertThat(firstPass.get("com.example.hierarchy.BranchA1SmConfig").transitionCount()).isEqualTo(3);
|
|
||||||
assertThat(firstPass.get("com.example.hierarchy.BranchA1SmConfig").eventNames())
|
|
||||||
.containsExactlyInAnyOrderElementsOf(TwelveConfigHierarchyTestSupport.expectedEventsForConcrete("A", 1));
|
|
||||||
assertThat(firstPass.get("com.example.hierarchy.BranchC4SmConfig").eventNames())
|
|
||||||
.containsExactlyInAnyOrderElementsOf(TwelveConfigHierarchyTestSupport.expectedEventsForConcrete("C", 4));
|
|
||||||
|
|
||||||
// Branch A and branch C machines must differ even when they share the same index.
|
|
||||||
assertThat(firstPass.get("com.example.hierarchy.BranchA2SmConfig"))
|
|
||||||
.isNotEqualTo(firstPass.get("com.example.hierarchy.BranchC2SmConfig"));
|
|
||||||
|
|
||||||
// Call-graph cache must survive enriching all twelve configs (same graph instance on second pass).
|
|
||||||
if (cachedGraphAfterFirstPass != null) {
|
|
||||||
assertThat(context.getCache().get("jdtCallGraph")).isSameAs(cachedGraphAfterFirstPass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static AnalysisResult analysisWithChain(String machineName, CallChain chain, Transition transition) {
|
|
||||||
return AnalysisResult.builder()
|
|
||||||
.name(machineName)
|
|
||||||
.transitions(List.of(transition))
|
|
||||||
.metadata(click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
|
|
||||||
.callChains(List.of(chain))
|
|
||||||
.build())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, Snapshot> enrichAllConfigs(
|
|
||||||
CodebaseContext context,
|
|
||||||
JdtIntelligenceProvider intelligence,
|
|
||||||
EnrichmentService enrichmentService,
|
|
||||||
List<String> configNames) {
|
|
||||||
Map<String, Snapshot> snapshots = new LinkedHashMap<>();
|
|
||||||
StateMachineAggregator aggregator = new StateMachineAggregator(context);
|
|
||||||
|
|
||||||
for (String configName : configNames) {
|
|
||||||
TypeDeclaration configType = context.getTypeDeclaration(configName);
|
|
||||||
if (configType == null) {
|
|
||||||
configType = context.getTypeDeclarations().stream()
|
|
||||||
.filter(td -> td.getName().getIdentifier().equals(configName))
|
|
||||||
.findFirst()
|
|
||||||
.orElse(null);
|
|
||||||
}
|
|
||||||
assertThat(configType).as("config type %s", configName).isNotNull();
|
|
||||||
|
|
||||||
java.util.List<Transition> transitions = aggregator.aggregateTransitions(configType);
|
|
||||||
AnalysisResult result = AnalysisResult.builder()
|
|
||||||
.name(configName)
|
|
||||||
.transitions(transitions)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
enrichmentService.enrich(result, context, intelligence);
|
|
||||||
snapshots.put(context.getFqn(configType), Snapshot.from(result));
|
|
||||||
}
|
|
||||||
return snapshots;
|
|
||||||
}
|
|
||||||
|
|
||||||
private record Snapshot(int transitionCount, List<String> eventNames) {
|
|
||||||
static Snapshot from(AnalysisResult result) {
|
|
||||||
List<String> events = result.getTransitions().stream()
|
|
||||||
.map(t -> t.getEvent() != null ? t.getEvent().rawName() : null)
|
|
||||||
.filter(java.util.Objects::nonNull)
|
|
||||||
.map(Snapshot::stripLiteralQuotes)
|
|
||||||
.sorted()
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
return new Snapshot(result.getTransitions().size(), events);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String stripLiteralQuotes(String name) {
|
|
||||||
if (name.length() >= 2 && name.startsWith("\"") && name.endsWith("\"")) {
|
|
||||||
return name.substring(1, name.length() - 1);
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user