Java improvements
This commit is contained in:
@@ -6,7 +6,8 @@
|
|||||||
exceptions. If you need to ask something, use `AskUserQuestion`. If you need clarification, use `AskUserQuestion`. If
|
exceptions. If you need to ask something, use `AskUserQuestion`. If you need clarification, use `AskUserQuestion`. If
|
||||||
you need a decision, use `AskUserQuestion`. The tool provides a freetext option automatically — use it.
|
you need a decision, use `AskUserQuestion`. The tool provides a freetext option automatically — use it.
|
||||||
* **Never assume anything about the user's intent.** Ask questions and wait for the user to clarify.
|
* **Never assume anything about the user's intent.** Ask questions and wait for the user to clarify.
|
||||||
* **Always use agentic code** see x-docs/mcp-api-usage-ac-implementation.md to understand the code, get an overview and
|
* **Always use agentic code** see x-docs/agent-api-usage-ac-implementation.md to understand the code, get an overview
|
||||||
|
and
|
||||||
if somethinmg is missing or not working, report it directly. Also. find identifier via agentic code. if agentic code
|
if somethinmg is missing or not working, report it directly. Also. find identifier via agentic code. if agentic code
|
||||||
is not running, report it.
|
is not running, report it.
|
||||||
* **NEVER start Docker yourself — the human starts it.** If you need the server/Neo4j for analysis and
|
* **NEVER start Docker yourself — the human starts it.** If you need the server/Neo4j for analysis and
|
||||||
@@ -45,7 +46,8 @@
|
|||||||
- All features must be tracked in `x-docs/roadmap.md`. Once a feature has been implemented, mark it `[x]` and add a
|
- All features must be tracked in `x-docs/roadmap.md`. Once a feature has been implemented, mark it `[x]` and add a
|
||||||
timestamp (date) indicating when it was completed.
|
timestamp (date) indicating when it was completed.
|
||||||
- For **every feature** that changes API behavior or what an agent can query, you MUST update
|
- For **every feature** that changes API behavior or what an agent can query, you MUST update
|
||||||
`x-docs/mcp-api-usage-ac-implementation.md` to reflect it (new/changed endpoints, response fields, semantics) — treat
|
`x-docs/agent-api-usage-ac-implementation.md` to reflect it (new/changed endpoints, response fields, semantics) —
|
||||||
|
treat
|
||||||
this doc
|
this doc
|
||||||
update as part of the feature's Definition of Done, not an optional follow-up.
|
update as part of the feature's Definition of Done, not an optional follow-up.
|
||||||
- Never add a `Co-Authored-By:` line (or any AI-attribution trailer) to commit messages.
|
- Never add a `Co-Authored-By:` line (or any AI-attribution trailer) to commit messages.
|
||||||
@@ -53,7 +55,7 @@
|
|||||||
server runs at `http://localhost:8787` with this repo already ingested as project `ac`. Prefer it
|
server runs at `http://localhost:8787` with this repo already ingested as project `ac`. Prefer it
|
||||||
over grep/Explore for call graphs, callers/callees, DB access, dataflow, and module overviews — it's
|
over grep/Explore for call graphs, callers/callees, DB access, dataflow, and module overviews — it's
|
||||||
exactly the tool this project builds, so using it here is both faster and the best test of its own
|
exactly the tool this project builds, so using it here is both faster and the best test of its own
|
||||||
output. Usage guide: `x-docs/mcp-api-usage-ac-implementation.md`. Re-ingest after code changes
|
output. Usage guide: `x-docs/agent-api-usage-ac-implementation.md`. Re-ingest after code changes
|
||||||
(`ac refresh` / `POST /api/projects/ac/refresh`, or `--deep`/`?deep=true` for a full field-level
|
(`ac refresh` / `POST /api/projects/ac/refresh`, or `--deep`/`?deep=true` for a full field-level
|
||||||
pass) before trusting query results.
|
pass) before trusting query results.
|
||||||
- **Tool priority: REST API first, then the `ac` CLI, then grep/Explore.** Query the REST endpoints
|
- **Tool priority: REST API first, then the `ac` CLI, then grep/Explore.** Query the REST endpoints
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ The server exposes every query capability as REST endpoints under `http://localh
|
|||||||
`/flow-backward`, `/variables/{n}/field-flow`, `/variables/{n}/reads` and `/writes`,
|
`/flow-backward`, `/variables/{n}/field-flow`, `/variables/{n}/reads` and `/writes`,
|
||||||
`/dynamic-calls/unresolved`, `/dynamic-calls/overrides`, `/refresh`, and more. The OpenAPI spec is served at
|
`/dynamic-calls/unresolved`, `/dynamic-calls/overrides`, `/refresh`, and more. The OpenAPI spec is served at
|
||||||
`/q/openapi`. A full usage guide with response fields and semantics lives in [
|
`/q/openapi`. A full usage guide with response fields and semantics lives in [
|
||||||
`x-docs/mcp-api-usage-ac-implementation.md`](x-docs/mcp-api-usage-ac-implementation.md).
|
`x-docs/agent-api-usage-ac-implementation.md`](x-docs/agent-api-usage-ac-implementation.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
server.url=http://localhost:8787
|
server.url=http://localhost:8787
|
||||||
# Stamped by manage-ac.sh (stamp_cli_version) from ac-code-server's agenticcode.version
|
# Stamped by manage-ac.sh (stamp_cli_version) from ac-code-server's agenticcode.version
|
||||||
# at build time. "dev" means this jar wasn't built via manage-ac.sh.
|
# at build time. "dev" means this jar wasn't built via manage-ac.sh.
|
||||||
version=176
|
version=182
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ quarkus.http.port=8787
|
|||||||
# AgenticCode's own release counter (not the Maven project version) — bump this by hand for each
|
# AgenticCode's own release counter (not the Maven project version) — bump this by hand for each
|
||||||
# release. Single source of truth for the startup log line, GET /api/version, and the OpenAPI
|
# release. Single source of truth for the startup log line, GET /api/version, and the OpenAPI
|
||||||
# info version (referenced below via property expression, not duplicated).
|
# info version (referenced below via property expression, not duplicated).
|
||||||
agenticcode.version=176
|
agenticcode.version=182
|
||||||
# OpenAPI / Swagger UI (item 48) — the generated spec is the contract the web-UI TS client
|
# OpenAPI / Swagger UI (item 48) — the generated spec is the contract the web-UI TS client
|
||||||
# is generated against. Served at /q/openapi (yaml/json); Swagger UI at /q/swagger-ui in dev.
|
# is generated against. Served at /q/openapi (yaml/json); Swagger UI at /q/swagger-ui in dev.
|
||||||
mp.openapi.extensions.smallrye.info.title=AgenticCode API
|
mp.openapi.extensions.smallrye.info.title=AgenticCode API
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import java.io.UncheckedIOException;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static io.restassured.RestAssured.given;
|
import static io.restassured.RestAssured.given;
|
||||||
import static org.hamcrest.Matchers.hasItem;
|
import static org.hamcrest.Matchers.*;
|
||||||
import static org.hamcrest.Matchers.not;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,6 +77,19 @@ class InheritedFieldCallIT {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
""");
|
""");
|
||||||
|
// A receiver that resolves to nothing at all: the marker for it can never be rewired, so it
|
||||||
|
// is the case the cleanup has to catch. It used to survive and be served from /callees.
|
||||||
|
write("DanglingLogic.java", """
|
||||||
|
package p;
|
||||||
|
public class DanglingLogic {
|
||||||
|
public void work(Object o) {
|
||||||
|
somethingUndeclared.doIt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
// Item 118/B (lambda and catch parameters are bound names, not inherited fields) is covered by
|
||||||
|
// JavaParserTest#boundNamesAreNotTakenForInheritedFieldReceivers, not here: the cleanup deletes
|
||||||
|
// every marker, so an end-to-end assertion would pass whether or not the marker was created.
|
||||||
given().contentType("application/json")
|
given().contentType("application/json")
|
||||||
.body(new ProjectResource.ProjectRequest(null, root.toString(), null, "java", null, null))
|
.body(new ProjectResource.ProjectRequest(null, root.toString(), null, "java", null, null))
|
||||||
.when().post("/api/projects/" + PROJECT)
|
.when().post("/api/projects/" + PROJECT)
|
||||||
@@ -110,20 +123,56 @@ class InheritedFieldCallIT {
|
|||||||
.body("items.name", hasItem("p.GrandChildLogic"));
|
.body("items.name", hasItem("p.GrandChildLogic"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These two assertions replace a pair that tested {@code STARTS WITH 'field:'} and
|
||||||
|
* {@code hasItem("field:repo")}. The prefix constant carried a stray {@code U+0001}, so the real
|
||||||
|
* markers were named {@code field:*} — the tests were true because they matched nothing,
|
||||||
|
* while 202 markers survived in a real project and one was served from {@code /callees}.
|
||||||
|
*
|
||||||
|
* <p>Hence: match the marker anywhere in the name (a ':' cannot occur in a module name), and
|
||||||
|
* assert the underlying invariant separately — a module name never contains a control character.
|
||||||
|
* Either one alone can be satisfied by a name the author did not anticipate.
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
void placeholderMarkersDoNotSurviveEnrichment() {
|
void noMarkerSurvivesEnrichmentUnderAnyName() {
|
||||||
try (Session session = driver.session()) {
|
try (Session session = driver.session()) {
|
||||||
long leftovers = session.run(
|
long leftovers = session.run(
|
||||||
"MATCH (m:MODULE {project: $p}) WHERE m.name STARTS WITH 'field:' RETURN count(m) AS c",
|
"MATCH (m:MODULE {project: $p}) WHERE m.name CONTAINS 'field:' RETURN count(m) AS c",
|
||||||
Map.of("p", PROJECT)).single().get("c").asLong();
|
Map.of("p", PROJECT)).single().get("c").asLong();
|
||||||
assertEquals(0, leftovers, "the field:* receiver markers are scaffolding and must be cleaned up");
|
assertEquals(0, leftovers, "the field: receiver markers are scaffolding and must be cleaned up");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void noModuleNameContainsAControlCharacter() {
|
||||||
|
try (Session session = driver.session()) {
|
||||||
|
List<String> odd = session.run(
|
||||||
|
"MATCH (m:MODULE {project: $p}) RETURN m.name AS n", Map.of("p", PROJECT))
|
||||||
|
.list(r -> r.get("n").asString()).stream()
|
||||||
|
.filter(n -> n.chars().anyMatch(c -> c < 0x20))
|
||||||
|
.toList();
|
||||||
|
assertEquals(List.of(), odd, "a module name is an identity an agent passes back in a URL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The leak was visible through the API, not only in the graph — which is why the graph-only
|
||||||
|
* assertions above are not enough on their own. {@code somethingUndeclared} resolves to nothing,
|
||||||
|
* so its marker is the one that used to survive.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void anUnresolvableReceiverDoesNotLeakIntoCallees() {
|
||||||
|
List<String> callees = given().when().get("/api/projects/" + PROJECT + "/modules/p.DanglingLogic/callees")
|
||||||
|
.then().statusCode(200)
|
||||||
|
.extract().jsonPath().getList("items.name", String.class);
|
||||||
|
assertEquals(List.of(), callees.stream().filter(n -> n != null && n.contains("field:")).toList(),
|
||||||
|
"internal scaffolding must never reach an API response");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void theMarkerIsNotExposedAsAModule() {
|
void theMarkerIsNotExposedAsAModule() {
|
||||||
given().when().get("/api/projects/" + PROJECT + "/modules?limit=100")
|
given().when().get("/api/projects/" + PROJECT + "/modules?limit=100")
|
||||||
.then().statusCode(200)
|
.then().statusCode(200)
|
||||||
.body("name", not(hasItem("field:repo")));
|
.body("name", not(hasItem(containsString("field:"))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -698,7 +698,11 @@ public final class CypherQueries {
|
|||||||
public static final String RESOLVE_SIMPLE_NAME_REFERENCES = """
|
public static final String RESOLVE_SIMPLE_NAME_REFERENCES = """
|
||||||
MATCH (ph:MODULE {project: $project})
|
MATCH (ph:MODULE {project: $project})
|
||||||
WHERE ph.sourceFile = '' AND NOT ph.name CONTAINS '.'
|
WHERE ph.sourceFile = '' AND NOT ph.name CONTAINS '.'
|
||||||
AND NOT ph.name STARTS WITH 'field:'
|
// CONTAINS, not STARTS WITH: a ':' cannot occur in a Java or Natural module name,
|
||||||
|
// so this is equally sharp — but it also catches a marker written by an older
|
||||||
|
// ingest, which a refresh would otherwise never clear (placeholders have no
|
||||||
|
// sourceFile, so the per-file sweeps do not reach them).
|
||||||
|
AND NOT ph.name CONTAINS 'field:'
|
||||||
MATCH (real:MODULE {project: $project, simpleName: ph.name})
|
MATCH (real:MODULE {project: $project, simpleName: ph.name})
|
||||||
WHERE real.sourceFile <> ''
|
WHERE real.sourceFile <> ''
|
||||||
WITH ph, collect(DISTINCT real) AS candidates
|
WITH ph, collect(DISTINCT real) AS candidates
|
||||||
@@ -779,10 +783,18 @@ public final class CypherQueries {
|
|||||||
* {@link #RESOLVE_INHERITED_FIELD_RECEIVERS} could not resolve — the receiver may be a static
|
* {@link #RESOLVE_INHERITED_FIELD_RECEIVERS} could not resolve — the receiver may be a static
|
||||||
* import, an outer-class field already handled at parse time (116a), or a type absent from the
|
* import, an outer-class field already handled at parse time (116a), or a type absent from the
|
||||||
* project. Leaving them would put {@code field:partnerRepository} into the module namespace.
|
* project. Leaving them would put {@code field:partnerRepository} into the module namespace.
|
||||||
|
*
|
||||||
|
* <p>Matched with {@code CONTAINS}, deliberately. The prefix constant once carried a stray
|
||||||
|
* {@code U+0001}, so a {@code STARTS WITH 'field:'} predicate matched nothing at all: 202 markers
|
||||||
|
* survived in one project, 180 of them still wired, and {@code field:e} was served from
|
||||||
|
* {@code /callees}. The two guards written against that literal — here and in
|
||||||
|
* {@link #RESOLVE_SIMPLE_NAME_REFERENCES} — were both silently dead. A ':' cannot occur in a
|
||||||
|
* module name, so matching anywhere is just as precise and survives a marker written by any
|
||||||
|
* earlier build.
|
||||||
*/
|
*/
|
||||||
public static final String DELETE_UNRESOLVED_FIELD_RECEIVERS = """
|
public static final String DELETE_UNRESOLVED_FIELD_RECEIVERS = """
|
||||||
MATCH (ph:MODULE {project: $project})
|
MATCH (ph:MODULE {project: $project})
|
||||||
WHERE ph.sourceFile = '' AND ph.name STARTS WITH 'field:'
|
WHERE ph.sourceFile = '' AND ph.name CONTAINS 'field:'
|
||||||
DETACH DELETE ph
|
DETACH DELETE ph
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
|||||||
@@ -154,19 +154,36 @@ public final class JavaParser implements LanguageParser {
|
|||||||
* never collide with a real class name, and so the cleanup step can find every leftover.
|
* never collide with a real class name, and so the cleanup step can find every leftover.
|
||||||
*/
|
*/
|
||||||
public static final String UNRESOLVED_FIELD_RECEIVER = "unresolvedFieldReceiver";
|
public static final String UNRESOLVED_FIELD_RECEIVER = "unresolvedFieldReceiver";
|
||||||
public static final String UNRESOLVED_FIELD_RECEIVER_PREFIX = "field:";
|
public static final String UNRESOLVED_FIELD_RECEIVER_PREFIX = "field:";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return whether {@code scope} is a bare lower-case identifier that this class does not declare —
|
* @return whether {@code scope} is a bare lower-case identifier that this class does not declare —
|
||||||
* i.e. it reads like a field rather than a type. Upper-case bare names are static calls and are
|
* i.e. it reads like a field rather than a type. Upper-case bare names are static calls and are
|
||||||
* already handled; anything with a declared type resolved before we got here.
|
* already handled; anything with a declared type resolved before we got here.
|
||||||
*/
|
*/
|
||||||
private static boolean isProbableFieldReceiver(Expression scope, Map<String, String> declaredTypes) {
|
/**
|
||||||
|
* True when a call's receiver is most likely a field this class does not declare itself — i.e.
|
||||||
|
* one inherited from a supertype in another file (item 116b).
|
||||||
|
*
|
||||||
|
* <p>{@code locallyBound} is the exclusion that makes this usable: every name the enclosing
|
||||||
|
* callable binds, including the parameters of nested lambdas and {@code catch} clauses. Without
|
||||||
|
* it, {@code .map(e -> e.getX())} looked like an inherited field named {@code e} — one class in a
|
||||||
|
* real codebase produced 24 such markers, and none of them could ever resolve.
|
||||||
|
*
|
||||||
|
* <p>Deliberately not folded into {@code declaredTypes}: an implicit lambda parameter has type
|
||||||
|
* {@code UnknownType}, and feeding that to {@link #resolveReceiverClass} would turn a silent
|
||||||
|
* omission into a confident edge to a module named after a non-type. The two questions are
|
||||||
|
* separate — {@code locallyBound} answers "is this a field?", {@code declaredTypes} answers
|
||||||
|
* "which type is it?".
|
||||||
|
*/
|
||||||
|
private static boolean isProbableFieldReceiver(Expression scope, Map<String, String> declaredTypes,
|
||||||
|
Set<String> locallyBound) {
|
||||||
if (!scope.isNameExpr()) {
|
if (!scope.isNameExpr()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
String name = scope.asNameExpr().getNameAsString();
|
String name = scope.asNameExpr().getNameAsString();
|
||||||
return !name.isEmpty() && Character.isLowerCase(name.charAt(0)) && !declaredTypes.containsKey(name);
|
return !name.isEmpty() && Character.isLowerCase(name.charAt(0))
|
||||||
|
&& !declaredTypes.containsKey(name) && !locallyBound.contains(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -242,7 +259,7 @@ public final class JavaParser implements LanguageParser {
|
|||||||
* {@code L} suffix); references to another constant in the same class (bare {@code NAME} or
|
* {@code L} suffix); references to another constant in the same class (bare {@code NAME} or
|
||||||
* {@code ThisClass.NAME}) are resolved transitively.
|
* {@code ThisClass.NAME}) are resolved transitively.
|
||||||
*/
|
*/
|
||||||
private static Map<String, String> collectConstants(ClassOrInterfaceDeclaration type) {
|
private static Map<String, String> collectConstants(TypeDeclaration<?> type) {
|
||||||
String className = type.getNameAsString();
|
String className = type.getNameAsString();
|
||||||
Map<String, Expression> initializers = new LinkedHashMap<>();
|
Map<String, Expression> initializers = new LinkedHashMap<>();
|
||||||
for (FieldDeclaration field : type.getFields()) {
|
for (FieldDeclaration field : type.getFields()) {
|
||||||
@@ -334,7 +351,7 @@ public final class JavaParser implements LanguageParser {
|
|||||||
* Resolves the table name from {@code @Entity(name = ...)} / {@code @Table(name = ...)},
|
* Resolves the table name from {@code @Entity(name = ...)} / {@code @Table(name = ...)},
|
||||||
* resolving a constant reference via {@code constants}, falling back to the class name.
|
* resolving a constant reference via {@code constants}, falling back to the class name.
|
||||||
*/
|
*/
|
||||||
private static String resolveTableName(ClassOrInterfaceDeclaration type, String className,
|
private static String resolveTableName(TypeDeclaration<?> type, String className,
|
||||||
Map<String, String> constants) {
|
Map<String, String> constants) {
|
||||||
for (String annotationName : List.of("Table", "Entity")) {
|
for (String annotationName : List.of("Table", "Entity")) {
|
||||||
@Nullable AnnotationExpr ann = annotation(type, annotationName).orElse(null);
|
@Nullable AnnotationExpr ann = annotation(type, annotationName).orElse(null);
|
||||||
@@ -390,7 +407,7 @@ public final class JavaParser implements LanguageParser {
|
|||||||
return props;
|
return props;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Optional<AnnotationExpr> annotation(ClassOrInterfaceDeclaration type, String name) {
|
private static Optional<AnnotationExpr> annotation(TypeDeclaration<?> type, String name) {
|
||||||
return type.getAnnotationByName(name);
|
return type.getAnnotationByName(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,15 +762,37 @@ public final class JavaParser implements LanguageParser {
|
|||||||
* parser cannot see (it parses one file) — that needs the enrichment stage that already resolves
|
* parser cannot see (it parses one file) — that needs the enrichment stage that already resolves
|
||||||
* the type hierarchy for {@code INJECTS}, and is tracked separately.
|
* the type hierarchy for {@code INJECTS}, and is tracked separately.
|
||||||
*/
|
*/
|
||||||
private static Map<String, String> enclosingFieldTypes(ClassOrInterfaceDeclaration type) {
|
/**
|
||||||
Deque<ClassOrInterfaceDeclaration> outermostFirst = new ArrayDeque<>();
|
* The type lexically enclosing {@code type}, of any kind — a class nested in a record or an enum
|
||||||
for (ClassOrInterfaceDeclaration enclosing = type.findAncestor(ClassOrInterfaceDeclaration.class).orElse(null);
|
* counts. Item 119: written as a helper rather than {@code findAncestor(TypeDeclaration.class)}
|
||||||
|
* inline, because the wildcard makes the generic form unwieldy at each call site.
|
||||||
|
*/
|
||||||
|
|
||||||
|
private static TypeFacts facts(TypeDeclaration<?> type) {
|
||||||
|
return switch (type) {
|
||||||
|
case ClassOrInterfaceDeclaration c -> new TypeFacts(c.isInterface() ? "INTERFACE" : "CLASS",
|
||||||
|
c.isInterface(), c.getExtendedTypes(), c.getImplementedTypes());
|
||||||
|
case EnumDeclaration e -> new TypeFacts("ENUM", false, List.of(), e.getImplementedTypes());
|
||||||
|
case RecordDeclaration r -> new TypeFacts("RECORD", false, List.of(), r.getImplementedTypes());
|
||||||
|
case AnnotationDeclaration a -> new TypeFacts("ANNOTATION", false, List.of(), List.of());
|
||||||
|
default -> new TypeFacts("CLASS", false, List.of(), List.of());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static TypeDeclaration<?> enclosingType(TypeDeclaration<?> type) {
|
||||||
|
return type.findAncestor(TypeDeclaration.class).map(t -> (TypeDeclaration<?>) t).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> enclosingFieldTypes(TypeDeclaration<?> type) {
|
||||||
|
Deque<TypeDeclaration<?>> outermostFirst = new ArrayDeque<>();
|
||||||
|
for (TypeDeclaration<?> enclosing = enclosingType(type);
|
||||||
enclosing != null;
|
enclosing != null;
|
||||||
enclosing = enclosing.findAncestor(ClassOrInterfaceDeclaration.class).orElse(null)) {
|
enclosing = enclosingType(enclosing)) {
|
||||||
outermostFirst.addFirst(enclosing);
|
outermostFirst.addFirst(enclosing);
|
||||||
}
|
}
|
||||||
Map<String, String> types = new HashMap<>();
|
Map<String, String> types = new HashMap<>();
|
||||||
for (ClassOrInterfaceDeclaration enclosing : outermostFirst) {
|
for (TypeDeclaration<?> enclosing : outermostFirst) {
|
||||||
for (FieldDeclaration field : enclosing.getFields()) {
|
for (FieldDeclaration field : enclosing.getFields()) {
|
||||||
for (VariableDeclarator variable : field.getVariables()) {
|
for (VariableDeclarator variable : field.getVariables()) {
|
||||||
types.put(variable.getNameAsString(), variable.getTypeAsString());
|
types.put(variable.getNameAsString(), variable.getTypeAsString());
|
||||||
@@ -771,7 +810,7 @@ public final class JavaParser implements LanguageParser {
|
|||||||
* position ({@code super(XStep.class, …)}, {@code batchlet(refName(X.class))}). Targets are
|
* position ({@code super(XStep.class, …)}, {@code batchlet(refName(X.class))}). Targets are
|
||||||
* deduped placeholder modules resolved to real modules by the finalize step.
|
* deduped placeholder modules resolved to real modules by the finalize step.
|
||||||
*/
|
*/
|
||||||
private static void addWiringEdges(ClassOrInterfaceDeclaration type, AstNode typeNode,
|
private static void addWiringEdges(TypeDeclaration<?> type, AstNode typeNode,
|
||||||
Map<String, AstNode> referencedModules, TypeResolver types,
|
Map<String, AstNode> referencedModules, TypeResolver types,
|
||||||
List<AstNode> nodes, List<AstEdge> edges) {
|
List<AstNode> nodes, List<AstEdge> edges) {
|
||||||
for (FieldDeclaration field : type.getFields()) {
|
for (FieldDeclaration field : type.getFields()) {
|
||||||
@@ -816,6 +855,10 @@ public final class JavaParser implements LanguageParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean hasCdiScope(TypeDeclaration<?> type) {
|
||||||
|
return type.getAnnotations().stream().anyMatch(a -> CDI_SCOPES.contains(a.getNameAsString()));
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isPlausibleEntityType(String type) {
|
private static boolean isPlausibleEntityType(String type) {
|
||||||
return !NON_ENTITY_RETURN_TYPES.contains(type) && !NON_DB_RECEIVER_TYPES.contains(type);
|
return !NON_ENTITY_RETURN_TYPES.contains(type) && !NON_DB_RECEIVER_TYPES.contains(type);
|
||||||
}
|
}
|
||||||
@@ -966,10 +1009,6 @@ public final class JavaParser implements LanguageParser {
|
|||||||
// DI + class-literal wiring edges (item J2)
|
// DI + class-literal wiring edges (item J2)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
private static boolean hasCdiScope(ClassOrInterfaceDeclaration type) {
|
|
||||||
return type.getAnnotations().stream().anyMatch(a -> CDI_SCOPES.contains(a.getNameAsString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ParseResult parse(String sourceFile, String content) {
|
public ParseResult parse(String sourceFile, String content) {
|
||||||
List<AstNode> nodes = new ArrayList<>();
|
List<AstNode> nodes = new ArrayList<>();
|
||||||
@@ -982,7 +1021,8 @@ public final class JavaParser implements LanguageParser {
|
|||||||
|
|
||||||
TypeResolver types = new TypeResolver(unit);
|
TypeResolver types = new TypeResolver(unit);
|
||||||
|
|
||||||
for (ClassOrInterfaceDeclaration type : unit.findAll(ClassOrInterfaceDeclaration.class)) {
|
for (TypeDeclaration<?> type : unit.findAll(TypeDeclaration.class)) {
|
||||||
|
TypeFacts facts = facts(type);
|
||||||
String className = type.getNameAsString();
|
String className = type.getNameAsString();
|
||||||
// Item 117: the fully-qualified name IS the module's identity — a simple name does not
|
// Item 117: the fully-qualified name IS the module's identity — a simple name does not
|
||||||
// identify a class (nested @Nested classes, Builder, WorkingStorage: 8% of one real
|
// identify a class (nested @Nested classes, Builder, WorkingStorage: 8% of one real
|
||||||
@@ -994,34 +1034,39 @@ public final class JavaParser implements LanguageParser {
|
|||||||
moduleProps.put("simpleName", className);
|
moduleProps.put("simpleName", className);
|
||||||
moduleProps.put("fqn", fqn);
|
moduleProps.put("fqn", fqn);
|
||||||
// J3: distinguish interfaces from classes (interface -> implementation resolution).
|
// J3: distinguish interfaces from classes (interface -> implementation resolution).
|
||||||
moduleProps.put("isInterface", String.valueOf(type.isInterface()));
|
moduleProps.put("isInterface", String.valueOf(facts.isInterface()));
|
||||||
// Item 1: persisted module sub-kind for API filtering ("list all interfaces"). Enums
|
// Item 1 / item 119: persisted module sub-kind for API filtering ("list all interfaces").
|
||||||
// and records aren't parsed as MODULE nodes at all yet (a larger, separate feature —
|
// CLASS | INTERFACE | ENUM | RECORD | ANNOTATION.
|
||||||
// this only classifies what JavaParser already handles: ClassOrInterfaceDeclaration).
|
moduleProps.put("moduleKind", facts.kind());
|
||||||
moduleProps.put("moduleKind", type.isInterface() ? "INTERFACE" : "CLASS");
|
// The JPA/Panache heuristics below are class/interface notions — a record is never a
|
||||||
// J1: tag repository classes with their managed entity so the enrichment step can map a
|
// Panache repository, and generalizing them would apply guesswork to types the assumption
|
||||||
// repository method call to the entity's DB_TABLE.
|
// was never written for.
|
||||||
@Nullable String repositoryEntity = repositoryEntityType(type);
|
@Nullable String repositoryEntity = null;
|
||||||
if (repositoryEntity != null) {
|
if (type instanceof ClassOrInterfaceDeclaration cls) {
|
||||||
moduleProps.put("repositoryEntity", repositoryEntity);
|
// J1: tag repository classes with their managed entity so the enrichment step can map
|
||||||
}
|
// a repository method call to the entity's DB_TABLE.
|
||||||
// J7: a project base class that passes Panache-ness one level up to its subclasses
|
repositoryEntity = repositoryEntityType(cls);
|
||||||
// (e.g. AbstractPurRepository<Entity, Id> implements PanacheRepositoryBase<Entity, Id>).
|
|
||||||
// Tag it with its entity type parameter + own parameter list so the graph-side
|
|
||||||
// enrichment step can bind a concrete subclass's EXTENDS type argument to it.
|
|
||||||
@Nullable String panacheParam = panacheEntityTypeParam(type);
|
|
||||||
if (panacheParam != null) {
|
|
||||||
moduleProps.put("panacheEntityTypeParam", panacheParam);
|
|
||||||
moduleProps.put("typeParams", String.join(",", ownTypeParamNames(type)));
|
|
||||||
}
|
|
||||||
// J1b: a repository-named interface with no generic entity argument anywhere in sight
|
|
||||||
// (e.g. a project-specific IRiskRepository) — guess the entity from a method return type.
|
|
||||||
if (repositoryEntity == null && panacheParam == null
|
|
||||||
&& type.isInterface() && isRepositoryReceiverName(className)) {
|
|
||||||
repositoryEntity = fallbackRepositoryEntity(type);
|
|
||||||
if (repositoryEntity != null) {
|
if (repositoryEntity != null) {
|
||||||
moduleProps.put("repositoryEntity", repositoryEntity);
|
moduleProps.put("repositoryEntity", repositoryEntity);
|
||||||
}
|
}
|
||||||
|
// J7: a project base class that passes Panache-ness one level up to its subclasses
|
||||||
|
// (e.g. AbstractPurRepository<Entity, Id> implements PanacheRepositoryBase<Entity, Id>).
|
||||||
|
// Tag it with its entity type parameter + own parameter list so the graph-side
|
||||||
|
// enrichment step can bind a concrete subclass's EXTENDS type argument to it.
|
||||||
|
@Nullable String panacheParam = panacheEntityTypeParam(cls);
|
||||||
|
if (panacheParam != null) {
|
||||||
|
moduleProps.put("panacheEntityTypeParam", panacheParam);
|
||||||
|
moduleProps.put("typeParams", String.join(",", ownTypeParamNames(cls)));
|
||||||
|
}
|
||||||
|
// J1b: a repository-named interface with no generic entity argument anywhere in sight
|
||||||
|
// (e.g. a project-specific IRiskRepository) — guess the entity from a method return type.
|
||||||
|
if (repositoryEntity == null && panacheParam == null
|
||||||
|
&& cls.isInterface() && isRepositoryReceiverName(className)) {
|
||||||
|
repositoryEntity = fallbackRepositoryEntity(cls);
|
||||||
|
if (repositoryEntity != null) {
|
||||||
|
moduleProps.put("repositoryEntity", repositoryEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
type.getJavadoc().ifPresent(jd -> {
|
type.getJavadoc().ifPresent(jd -> {
|
||||||
String firstLine = jd.getDescription().toText().lines()
|
String firstLine = jd.getDescription().toText().lines()
|
||||||
@@ -1047,14 +1092,14 @@ public final class JavaParser implements LanguageParser {
|
|||||||
// duplicate nodes that collide on the (type, name, sourceFile, project) merge key.
|
// duplicate nodes that collide on the (type, name, sourceFile, project) merge key.
|
||||||
Map<String, AstNode> referencedModules = new HashMap<>();
|
Map<String, AstNode> referencedModules = new HashMap<>();
|
||||||
|
|
||||||
for (ClassOrInterfaceType extended : type.getExtendedTypes()) {
|
for (ClassOrInterfaceType extended : facts.extended()) {
|
||||||
AstNode superType = referencedModule(referencedModules, nodes, types.resolve(supertypeName(extended, className)));
|
AstNode superType = referencedModule(referencedModules, nodes, types.resolve(supertypeName(extended, className)));
|
||||||
Map<String, String> extendsProps = extendsTypeArgs(extended);
|
Map<String, String> extendsProps = extendsTypeArgs(extended);
|
||||||
edges.add(extendsProps.isEmpty()
|
edges.add(extendsProps.isEmpty()
|
||||||
? edge(EdgeType.EXTENDS, typeNode.id(), superType.id(), typeNode.startLine())
|
? edge(EdgeType.EXTENDS, typeNode.id(), superType.id(), typeNode.startLine())
|
||||||
: edge(EdgeType.EXTENDS, typeNode.id(), superType.id(), typeNode.startLine(), null, extendsProps));
|
: edge(EdgeType.EXTENDS, typeNode.id(), superType.id(), typeNode.startLine(), null, extendsProps));
|
||||||
}
|
}
|
||||||
for (ClassOrInterfaceType implemented : type.getImplementedTypes()) {
|
for (ClassOrInterfaceType implemented : facts.implemented()) {
|
||||||
AstNode interfaceType = referencedModule(referencedModules, nodes, types.resolve(supertypeName(implemented, className)));
|
AstNode interfaceType = referencedModule(referencedModules, nodes, types.resolve(supertypeName(implemented, className)));
|
||||||
edges.add(edge(EdgeType.IMPLEMENTS, typeNode.id(), interfaceType.id(), typeNode.startLine()));
|
edges.add(edge(EdgeType.IMPLEMENTS, typeNode.id(), interfaceType.id(), typeNode.startLine()));
|
||||||
}
|
}
|
||||||
@@ -1064,7 +1109,8 @@ public final class JavaParser implements LanguageParser {
|
|||||||
|
|
||||||
// JPA entity: resolve the table name and link the class to its DB_TABLE. A Panache
|
// JPA entity: resolve the table name and link the class to its DB_TABLE. A Panache
|
||||||
// active-record entity (extends PanacheEntity[Base]) is an entity even without @Entity.
|
// active-record entity (extends PanacheEntity[Base]) is an entity even without @Entity.
|
||||||
boolean isEntity = annotation(type, "Entity").isPresent() || extendsPanacheEntity(type);
|
boolean isEntity = annotation(type, "Entity").isPresent()
|
||||||
|
|| (type instanceof ClassOrInterfaceDeclaration cls && extendsPanacheEntity(cls));
|
||||||
boolean isMappedSuperclass = annotation(type, "MappedSuperclass").isPresent();
|
boolean isMappedSuperclass = annotation(type, "MappedSuperclass").isPresent();
|
||||||
if (isEntity && !isMappedSuperclass) {
|
if (isEntity && !isMappedSuperclass) {
|
||||||
String tableName = resolveTableName(type, className, constants);
|
String tableName = resolveTableName(type, className, constants);
|
||||||
@@ -1112,6 +1158,50 @@ public final class JavaParser implements LanguageParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Item 119: the state a record, an enum or an annotation type carries is not declared as a
|
||||||
|
// FieldDeclaration, so the loop above sees none of it. Without these three the types would
|
||||||
|
// be modules with an empty body — "analysed, nothing found" for a record DTO's components,
|
||||||
|
// which is the failure mode item 114 is about.
|
||||||
|
for (RecordDeclaration record : type instanceof RecordDeclaration r ? List.of(r) : List.<RecordDeclaration>of()) {
|
||||||
|
for (Parameter component : record.getParameters()) {
|
||||||
|
int line = component.getBegin().map(p -> p.line).orElse(typeNode.startLine());
|
||||||
|
AstNode componentNode = node(NodeType.FIELD, component.getNameAsString(), sourceFile,
|
||||||
|
line, line, component.getTypeAsString(), null,
|
||||||
|
Map.of("recordComponent", "true"));
|
||||||
|
nodes.add(componentNode);
|
||||||
|
edges.add(edge(EdgeType.CONTAINS, typeNode.id(), componentNode.id(), line));
|
||||||
|
fieldsByName.put(component.getNameAsString(), componentNode);
|
||||||
|
fieldTypes.put(component.getNameAsString(), component.getTypeAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (EnumDeclaration enumeration : type instanceof EnumDeclaration e ? List.of(e) : List.<EnumDeclaration>of()) {
|
||||||
|
for (EnumConstantDeclaration constant : enumeration.getEntries()) {
|
||||||
|
int line = constant.getBegin().map(p -> p.line).orElse(typeNode.startLine());
|
||||||
|
// The constant's own class body (a per-constant override) is not modelled as a
|
||||||
|
// separate module; its methods would need an identity no source-level name gives them.
|
||||||
|
AstNode constantNode = node(NodeType.CONSTANT, constant.getNameAsString(), sourceFile,
|
||||||
|
line, line, className, null, Map.of("enumConstant", "true"));
|
||||||
|
nodes.add(constantNode);
|
||||||
|
edges.add(edge(EdgeType.CONTAINS, typeNode.id(), constantNode.id(), line));
|
||||||
|
fieldsByName.put(constant.getNameAsString(), constantNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (AnnotationMemberDeclaration member : type.getMembers().stream()
|
||||||
|
.filter(AnnotationMemberDeclaration.class::isInstance)
|
||||||
|
.map(AnnotationMemberDeclaration.class::cast).toList()) {
|
||||||
|
int line = member.getBegin().map(p -> p.line).orElse(typeNode.startLine());
|
||||||
|
Map<String, String> memberProps = new HashMap<>();
|
||||||
|
memberProps.put("annotationMember", "true");
|
||||||
|
member.getDefaultValue().ifPresent(v -> memberProps.put("defaultValue", v.toString()));
|
||||||
|
// A FIELD, not a FUNCTION: the question asked of an annotation type is which attributes
|
||||||
|
// it carries, not which methods it declares.
|
||||||
|
AstNode memberNode = node(NodeType.FIELD, member.getNameAsString(), sourceFile,
|
||||||
|
line, line, member.getType().asString(), null, memberProps);
|
||||||
|
nodes.add(memberNode);
|
||||||
|
edges.add(edge(EdgeType.CONTAINS, typeNode.id(), memberNode.id(), line));
|
||||||
|
fieldsByName.put(member.getNameAsString(), memberNode);
|
||||||
|
}
|
||||||
|
|
||||||
// Functions: methods + constructors (constructors are FUNCTION nodes named after the class).
|
// Functions: methods + constructors (constructors are FUNCTION nodes named after the class).
|
||||||
Map<String, AstNode> methods = new HashMap<>();
|
Map<String, AstNode> methods = new HashMap<>();
|
||||||
List<CallableDeclaration<?>> callables = new ArrayList<>();
|
List<CallableDeclaration<?>> callables = new ArrayList<>();
|
||||||
@@ -1176,6 +1266,11 @@ public final class JavaParser implements LanguageParser {
|
|||||||
shadowed.add(v.getNameAsString());
|
shadowed.add(v.getNameAsString());
|
||||||
declaredTypes.put(v.getNameAsString(), v.getTypeAsString());
|
declaredTypes.put(v.getNameAsString(), v.getTypeAsString());
|
||||||
}));
|
}));
|
||||||
|
// Every *nested* parameter too — lambda and catch parameters. They are bound names
|
||||||
|
// like any local, but they are not VariableDeclarationExpr and not the callable's own
|
||||||
|
// parameter list, so both loops above miss them. Names only: an implicit lambda
|
||||||
|
// parameter has no usable declared type (see isProbableFieldReceiver).
|
||||||
|
callable.findAll(Parameter.class).forEach(p -> shadowed.add(p.getNameAsString()));
|
||||||
|
|
||||||
addFieldAccessEdges(callable, callableNode, fieldsByName, shadowed, edges);
|
addFieldAccessEdges(callable, callableNode, fieldsByName, shadowed, edges);
|
||||||
|
|
||||||
@@ -1196,7 +1291,7 @@ public final class JavaParser implements LanguageParser {
|
|||||||
@Nullable String simpleTarget = resolveReceiverClass(scope, declaredTypes);
|
@Nullable String simpleTarget = resolveReceiverClass(scope, declaredTypes);
|
||||||
@Nullable String targetClass = simpleTarget == null ? null : types.resolve(simpleTarget);
|
@Nullable String targetClass = simpleTarget == null ? null : types.resolve(simpleTarget);
|
||||||
// The DB-access candidate keeps the receiver as written (see extendsTypeArgs).
|
// The DB-access candidate keeps the receiver as written (see extendsTypeArgs).
|
||||||
if (targetClass == null && isProbableFieldReceiver(scope, declaredTypes)) {
|
if (targetClass == null && isProbableFieldReceiver(scope, declaredTypes, shadowed)) {
|
||||||
// Item 116b: the receiver names a field this class does not declare — almost
|
// Item 116b: the receiver names a field this class does not declare — almost
|
||||||
// always one inherited from a supertype, which lives in another file the
|
// always one inherited from a supertype, which lives in another file the
|
||||||
// parser never sees. Record the receiver's *name* against a placeholder so
|
// parser never sees. Record the receiver's *name* against a placeholder so
|
||||||
@@ -1242,6 +1337,27 @@ public final class JavaParser implements LanguageParser {
|
|||||||
return new ParseResult(nodes, edges);
|
return new ParseResult(nodes, edges);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The handful of facts that differ between the four kinds of Java type declaration, resolved once
|
||||||
|
* so the ingest loop below can treat them uniformly.
|
||||||
|
*
|
||||||
|
* <p>Item 119: before this, the loop iterated {@code ClassOrInterfaceDeclaration} only, and enums,
|
||||||
|
* records and annotation types were not modules at all — 168 types in one real codebase, with
|
||||||
|
* every reference to them left dangling as a placeholder. They are modelled here because the three
|
||||||
|
* things that genuinely differ are exactly these: only a class/interface can {@code extends},
|
||||||
|
* only an annotation type can do neither, and {@code isInterface} is a class/interface notion.
|
||||||
|
* Everything else ({@code getFields}, {@code getMethods}, {@code getConstructors},
|
||||||
|
* {@code getAnnotations}, {@code getJavadoc}, {@code getFullyQualifiedName}) is common.
|
||||||
|
*
|
||||||
|
* <p>One deliberate omission: a record's <em>compact</em> canonical constructor. It is a
|
||||||
|
* {@code CompactConstructorDeclaration}, which — unlike every other member — does not extend
|
||||||
|
* {@code CallableDeclaration}, so it does not fit the callable machinery below and its body's
|
||||||
|
* calls stay invisible. One occurrence in the codebase that motivated this.
|
||||||
|
*/
|
||||||
|
private record TypeFacts(String kind, boolean isInterface,
|
||||||
|
List<ClassOrInterfaceType> extended, List<ClassOrInterfaceType> implemented) {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if {@code classExpr} is a direct argument of a method call, {@code new}, or {@code super()}/{@code this()}.
|
* True if {@code classExpr} is a direct argument of a method call, {@code new}, or {@code super()}/{@code this()}.
|
||||||
*/
|
*/
|
||||||
@@ -1286,7 +1402,10 @@ public final class JavaParser implements LanguageParser {
|
|||||||
}
|
}
|
||||||
// Types declared in this file — including nested ones, whose FQN is the enclosing chain
|
// Types declared in this file — including nested ones, whose FQN is the enclosing chain
|
||||||
// (a.b.Outer.Inner), which is exactly what made same-simple-name nested classes collide.
|
// (a.b.Outer.Inner), which is exactly what made same-simple-name nested classes collide.
|
||||||
for (ClassOrInterfaceDeclaration declared : unit.findAll(ClassOrInterfaceDeclaration.class)) {
|
// Item 119: every kind of type declaration, not just class/interface — otherwise a
|
||||||
|
// reference to an enum or record declared in this very file cannot be qualified, which
|
||||||
|
// would be item 117 running backwards for exactly the types it just gained.
|
||||||
|
for (TypeDeclaration<?> declared : unit.findAll(TypeDeclaration.class)) {
|
||||||
declared.getFullyQualifiedName()
|
declared.getFullyQualifiedName()
|
||||||
.ifPresent(fqn -> declaredHere.put(declared.getNameAsString(), fqn));
|
.ifPresent(fqn -> declaredHere.put(declared.getNameAsString(), fqn));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import java.io.IOException;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
@@ -36,6 +37,12 @@ class JavaParserTest {
|
|||||||
&& e.sourceId().equals(source.id()) && e.targetId().equals(target.id()));
|
&& e.sourceId().equals(source.id()) && e.targetId().equals(target.id()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String prop(AstNode node, String key) {
|
||||||
|
Map<String, String> props = node.properties();
|
||||||
|
assertNotNull(props, "node " + node.name() + " has no properties");
|
||||||
|
return String.valueOf(props.get(key));
|
||||||
|
}
|
||||||
|
|
||||||
private static String readFixture(String name) throws IOException {
|
private static String readFixture(String name) throws IOException {
|
||||||
Path path = Path.of("src/test/resources/fixtures/java", name);
|
Path path = Path.of("src/test/resources/fixtures/java", name);
|
||||||
return Files.readString(path, StandardCharsets.UTF_8);
|
return Files.readString(path, StandardCharsets.UTF_8);
|
||||||
@@ -277,4 +284,93 @@ class JavaParserTest {
|
|||||||
assertFalse(hasEdgeToNode(result, EdgeType.READS, shadowing, orderCountField));
|
assertFalse(hasEdgeToNode(result, EdgeType.READS, shadowing, orderCountField));
|
||||||
assertFalse(hasEdgeToNode(result, EdgeType.WRITES, shadowing, orderCountField));
|
assertFalse(hasEdgeToNode(result, EdgeType.WRITES, shadowing, orderCountField));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item 118/B: a lambda, catch or for-each variable is a bound name, not a field inherited from a
|
||||||
|
* supertype. Treating it as one produced a marker module per variable — 24 in a single class of a
|
||||||
|
* real codebase, none of them ever resolvable.
|
||||||
|
*
|
||||||
|
* <p>This has to be asserted here rather than through the API: the enrichment cleanup deletes
|
||||||
|
* every marker, so a leftover is invisible downstream and an end-to-end test of this would pass
|
||||||
|
* whether or not the marker was created.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void boundNamesAreNotTakenForInheritedFieldReceivers() throws IOException {
|
||||||
|
LanguageParser.ParseResult result =
|
||||||
|
parser.parse("BoundNameReceivers.java", readFixture("BoundNameReceivers.java"));
|
||||||
|
|
||||||
|
String prefix = JavaParser.UNRESOLVED_FIELD_RECEIVER_PREFIX;
|
||||||
|
assertEquals(List.of(prefix + "inherited"),
|
||||||
|
result.nodes().stream()
|
||||||
|
.filter(n -> n.type() == NodeType.MODULE && n.name().startsWith(prefix))
|
||||||
|
.map(AstNode::name).sorted().toList(),
|
||||||
|
"only the genuinely undeclared receiver may be marked — not entry/element/ex");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The marker prefix is compared against in Cypher ({@code DELETE_UNRESOLVED_FIELD_RECEIVERS}), so
|
||||||
|
* a stray character in it disables the cleanup silently. That happened: a {@code U+0001} slipped
|
||||||
|
* in, 202 markers survived in one project and one was served from {@code /callees}.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Item 119: enums, records and annotation types are modules, identified by their FQN like every
|
||||||
|
* other type. Before this they were not parsed at all — 168 types in one real codebase, and every
|
||||||
|
* reference to one of them stayed a dangling placeholder.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void enumsRecordsAndAnnotationTypesAreModules() throws IOException {
|
||||||
|
LanguageParser.ParseResult result = parser.parse("TypeKinds.java", readFixture("TypeKinds.java"));
|
||||||
|
|
||||||
|
assertEquals("ENUM", prop(findNode(result, NodeType.MODULE, "com.example.sample.TypeKinds.Status"), "moduleKind"));
|
||||||
|
assertEquals("RECORD", prop(findNode(result, NodeType.MODULE, "com.example.sample.TypeKinds.Money"), "moduleKind"));
|
||||||
|
assertEquals("ANNOTATION", prop(findNode(result, NodeType.MODULE, "com.example.sample.TypeKinds.Audited"), "moduleKind"));
|
||||||
|
// A class nested in a record is reached too — the enclosing-type walk is not class-only.
|
||||||
|
findNode(result, NodeType.MODULE, "com.example.sample.TypeKinds.Money.Formatter");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state each kind carries is not a {@code FieldDeclaration}, so without explicit handling the
|
||||||
|
* modules above would have an empty body — a record DTO reporting no components is the
|
||||||
|
* "analysed, nothing found" answer item 114 is about.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void recordComponentsEnumConstantsAndAnnotationMembersAreCaptured() throws IOException {
|
||||||
|
LanguageParser.ParseResult result = parser.parse("TypeKinds.java", readFixture("TypeKinds.java"));
|
||||||
|
|
||||||
|
AstNode money = findNode(result, NodeType.MODULE, "com.example.sample.TypeKinds.Money");
|
||||||
|
assertTrue(hasEdge(result, EdgeType.CONTAINS, money, "amount", NodeType.FIELD));
|
||||||
|
assertTrue(hasEdge(result, EdgeType.CONTAINS, money, "currency", NodeType.FIELD));
|
||||||
|
assertEquals("long", findNode(result, NodeType.FIELD, "amount").dataType());
|
||||||
|
|
||||||
|
AstNode status = findNode(result, NodeType.MODULE, "com.example.sample.TypeKinds.Status");
|
||||||
|
assertTrue(hasEdge(result, EdgeType.CONTAINS, status, "OPEN", NodeType.CONSTANT));
|
||||||
|
assertTrue(hasEdge(result, EdgeType.CONTAINS, status, "CLOSED", NodeType.CONSTANT));
|
||||||
|
assertTrue(hasEdge(result, EdgeType.CONTAINS, status, "terminal", NodeType.FUNCTION));
|
||||||
|
|
||||||
|
AstNode audited = findNode(result, NodeType.MODULE, "com.example.sample.TypeKinds.Audited");
|
||||||
|
assertTrue(hasEdge(result, EdgeType.CONTAINS, audited, "value", NodeType.FIELD));
|
||||||
|
assertEquals("true", prop(findNode(result, NodeType.FIELD, "enabled"), "defaultValue"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An enum's or record's {@code implements} is a real edge — it feeds the CHA fan-out.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void enumsAndRecordsCarryTheirImplementsEdges() throws IOException {
|
||||||
|
LanguageParser.ParseResult result = parser.parse("TypeKinds.java", readFixture("TypeKinds.java"));
|
||||||
|
|
||||||
|
assertTrue(hasEdge(result, EdgeType.IMPLEMENTS,
|
||||||
|
findNode(result, NodeType.MODULE, "com.example.sample.TypeKinds.Status"),
|
||||||
|
"Comparable", NodeType.MODULE));
|
||||||
|
assertTrue(hasEdge(result, EdgeType.IMPLEMENTS,
|
||||||
|
findNode(result, NodeType.MODULE, "com.example.sample.TypeKinds.Money"),
|
||||||
|
"Comparable", NodeType.MODULE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void theMarkerPrefixIsPlainText() {
|
||||||
|
assertEquals("field:", JavaParser.UNRESOLVED_FIELD_RECEIVER_PREFIX);
|
||||||
|
assertFalse(JavaParser.UNRESOLVED_FIELD_RECEIVER_PREFIX.chars().anyMatch(c -> c < 0x20),
|
||||||
|
"a control character here silently disables every Cypher guard that matches on it");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.example.sample;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixture for item 118/B: names bound by a lambda, a catch clause and an enhanced-for are not
|
||||||
|
* inherited fields, and must not produce an unresolved-field-receiver marker.
|
||||||
|
*
|
||||||
|
* <p>{@code inherited} is the counter-case: it is declared nowhere in this file, so it really is a
|
||||||
|
* candidate for a field from a supertype and must keep producing a marker.
|
||||||
|
*/
|
||||||
|
public class BoundNameReceivers {
|
||||||
|
|
||||||
|
public void run(List<String> items) {
|
||||||
|
items.forEach(entry -> entry.trim());
|
||||||
|
for (String element : items) {
|
||||||
|
element.length();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
items.clear();
|
||||||
|
} catch (RuntimeException ex) {
|
||||||
|
ex.getMessage();
|
||||||
|
}
|
||||||
|
inherited.doWork();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.example.sample;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixture for item 119: the three declaration kinds that were not modules at all — enum, record and
|
||||||
|
* annotation type — plus a class nested in a record, to prove the enclosing-type walk is not
|
||||||
|
* class-only either.
|
||||||
|
*/
|
||||||
|
public class TypeKinds {
|
||||||
|
|
||||||
|
/** An enum with an interface, constants and a method. */
|
||||||
|
public enum Status implements Comparable<Status> {
|
||||||
|
OPEN,
|
||||||
|
CLOSED;
|
||||||
|
|
||||||
|
public boolean terminal() {
|
||||||
|
return this == CLOSED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A record with components, an interface and a normal method. */
|
||||||
|
public record Money(long amount, String currency) implements Comparable<Money> {
|
||||||
|
|
||||||
|
public String display() {
|
||||||
|
return amount + " " + currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(Money other) {
|
||||||
|
return Long.compare(amount, other.amount());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nested in a record — the enclosing-field walk must reach a record, not only a class. */
|
||||||
|
static class Formatter {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An annotation type with a member and a defaulted member. */
|
||||||
|
public @interface Audited {
|
||||||
|
String value();
|
||||||
|
|
||||||
|
boolean enabled() default true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Status> all() {
|
||||||
|
return List.of(Status.OPEN, Status.CLOSED);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
ac-ui/e2e/java-fqn.spec.ts
Normal file
46
ac-ui/e2e/java-fqn.spec.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import {expect, test} from "@playwright/test";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item 117 UI: a Java module's identity is its fully-qualified name, but the UI must not *show* it.
|
||||||
|
*
|
||||||
|
* The existing explorer spec runs against `upms` — Natural, whose names never contain a dot — so it
|
||||||
|
* passes either way and proves nothing here. These two run against a Java project, where the identity
|
||||||
|
* and the label genuinely differ.
|
||||||
|
*
|
||||||
|
* Depends on project `pur` being ingested with a post-117 build (`name` = FQN, `simpleName` set).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const FQN = "com.uniqagroup.common.base.AbstractLogic";
|
||||||
|
const SIMPLE = "AbstractLogic";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The regression this guards: with `name` holding the FQN, an anchored pattern on the class itself
|
||||||
|
* matched nothing, and the list looked empty rather than wrong.
|
||||||
|
*/
|
||||||
|
test("the module filter accepts the simple class name as well as the FQN", async ({page}) => {
|
||||||
|
await page.goto("/p/pur");
|
||||||
|
await expect(page.getByText(/\d{2,} modules/)).toBeVisible();
|
||||||
|
|
||||||
|
const filter = page.getByPlaceholder(/Filter name or file/);
|
||||||
|
|
||||||
|
await filter.fill(`^${SIMPLE}$`);
|
||||||
|
await expect(page.getByText(/^1 modules$/)).toBeVisible();
|
||||||
|
|
||||||
|
// The fully-qualified form still resolves — matching the short name is an addition, not a swap.
|
||||||
|
await filter.fill(`^${FQN}$`);
|
||||||
|
await expect(page.getByText(/^1 modules$/)).toBeVisible();
|
||||||
|
|
||||||
|
// And the package path stays searchable, which is the whole point of keeping `name` in the filter.
|
||||||
|
await filter.fill("common\\.base\\.");
|
||||||
|
await expect(page.getByText(/^[2-9]\d* modules$/)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** The module opens by its identity (the FQN) but is labelled with the short name. */
|
||||||
|
test("a Java module is addressed by FQN and displayed by simple name", async ({page}) => {
|
||||||
|
await page.goto(`/p/pur/m/${FQN}?tab=overview`);
|
||||||
|
|
||||||
|
const heading = page.getByRole("heading", {name: SIMPLE, exact: true});
|
||||||
|
await expect(heading).toBeVisible();
|
||||||
|
// The identity is not lost — it is one hover away.
|
||||||
|
await expect(heading).toHaveAttribute("title", FQN);
|
||||||
|
});
|
||||||
31
ac-ui/src/api/names.ts
Normal file
31
ac-ui/src/api/names.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Display names for module identities.
|
||||||
|
*
|
||||||
|
* <p>Since item 117 a Java module's identity — the `name` every endpoint takes and returns — is its
|
||||||
|
* fully-qualified name (`com.example.OrderService`, `a.b.Outer.Inner` for a nested class). That is the
|
||||||
|
* right key and the wrong label: it does not fit a list row, a tree line or a graph node.
|
||||||
|
*
|
||||||
|
* <p>Only the module *list* carries `simpleName`; `digest`, `graph`, `callers`, `callees` and
|
||||||
|
* `call-tree` return the identity alone. So where the server offers the short form we use it, and
|
||||||
|
* everywhere else we derive it. The derivation was checked against the server on a real codebase:
|
||||||
|
* for all 4734 qualified modules of one project, the text after the last dot equals `simpleName`.
|
||||||
|
*
|
||||||
|
* <p>Natural module names never contain a dot (verified across 3587 upms modules), so this is a
|
||||||
|
* no-op for them — as it is for projects ingested before 117, whose names are still short.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** The label to show for a module identity. Never use this as a key, a route or a request parameter. */
|
||||||
|
export function shortName(name: string | undefined | null): string {
|
||||||
|
if (!name) return "";
|
||||||
|
const dot = name.lastIndexOf(".");
|
||||||
|
return dot < 0 ? name : name.slice(dot + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same, but prefers the short form the server itself derived. The server knows the cases a string
|
||||||
|
* rule cannot: a class in the default package, and a local/anonymous class for which JavaParser
|
||||||
|
* reports no qualified name at all — both keep their plain name rather than losing a segment.
|
||||||
|
*/
|
||||||
|
export function displayName(name: string | undefined | null, simpleName?: string | null): string {
|
||||||
|
return simpleName || shortName(name);
|
||||||
|
}
|
||||||
@@ -579,6 +579,7 @@ export interface paths {
|
|||||||
fields?: string;
|
fields?: string;
|
||||||
followWiring?: boolean;
|
followWiring?: boolean;
|
||||||
resolveInterfaces?: boolean;
|
resolveInterfaces?: boolean;
|
||||||
|
sourceFile?: string;
|
||||||
};
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path: {
|
||||||
@@ -642,6 +643,7 @@ export interface paths {
|
|||||||
offset?: number;
|
offset?: number;
|
||||||
resolveInterfaces?: boolean;
|
resolveInterfaces?: boolean;
|
||||||
scope?: string;
|
scope?: string;
|
||||||
|
sourceFile?: string;
|
||||||
};
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path: {
|
||||||
@@ -704,6 +706,7 @@ export interface paths {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
scope?: string;
|
scope?: string;
|
||||||
|
sourceFile?: string;
|
||||||
};
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path: {
|
||||||
@@ -752,7 +755,9 @@ export interface paths {
|
|||||||
/** Entity Columns */
|
/** Entity Columns */
|
||||||
get: {
|
get: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: {
|
||||||
|
sourceFile?: string;
|
||||||
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -780,7 +785,7 @@ export interface paths {
|
|||||||
"application/json": components["schemas"]["ErrorResponse"];
|
"application/json": components["schemas"]["ErrorResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
/** @description Ambiguous module name (AMBIGUOUS_NAME, with the candidate sourceFiles in details) or an unresolved placeholder whose source is not ingested (NOT_INGESTED). */
|
||||||
409: {
|
409: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
@@ -813,6 +818,7 @@ export interface paths {
|
|||||||
include?: string;
|
include?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
sourceFile?: string;
|
||||||
};
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path: {
|
||||||
@@ -841,7 +847,7 @@ export interface paths {
|
|||||||
"application/json": components["schemas"]["ErrorResponse"];
|
"application/json": components["schemas"]["ErrorResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
/** @description Ambiguous module name (AMBIGUOUS_NAME, with the candidate sourceFiles in details) or an unresolved placeholder whose source is not ingested (NOT_INGESTED). */
|
||||||
409: {
|
409: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
@@ -870,7 +876,9 @@ export interface paths {
|
|||||||
/** Module Data Structures */
|
/** Module Data Structures */
|
||||||
get: {
|
get: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: {
|
||||||
|
sourceFile?: string;
|
||||||
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -898,7 +906,7 @@ export interface paths {
|
|||||||
"application/json": components["schemas"]["ErrorResponse"];
|
"application/json": components["schemas"]["ErrorResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
/** @description Ambiguous module name (AMBIGUOUS_NAME, with the candidate sourceFiles in details) or an unresolved placeholder whose source is not ingested (NOT_INGESTED). */
|
||||||
409: {
|
409: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
@@ -931,6 +939,7 @@ export interface paths {
|
|||||||
depth?: number;
|
depth?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
sourceFile?: string;
|
||||||
};
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path: {
|
||||||
@@ -959,7 +968,7 @@ export interface paths {
|
|||||||
"application/json": components["schemas"]["ErrorResponse"];
|
"application/json": components["schemas"]["ErrorResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
/** @description Ambiguous module name (AMBIGUOUS_NAME, with the candidate sourceFiles in details) or an unresolved placeholder whose source is not ingested (NOT_INGESTED). */
|
||||||
409: {
|
409: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
@@ -988,7 +997,9 @@ export interface paths {
|
|||||||
/** Module Digest */
|
/** Module Digest */
|
||||||
get: {
|
get: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: {
|
||||||
|
sourceFile?: string;
|
||||||
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -1045,7 +1056,9 @@ export interface paths {
|
|||||||
/** Dispatch Table */
|
/** Dispatch Table */
|
||||||
get: {
|
get: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: {
|
||||||
|
sourceFile?: string;
|
||||||
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -1073,7 +1086,7 @@ export interface paths {
|
|||||||
"application/json": components["schemas"]["ErrorResponse"];
|
"application/json": components["schemas"]["ErrorResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
/** @description Ambiguous module name (AMBIGUOUS_NAME, with the candidate sourceFiles in details) or an unresolved placeholder whose source is not ingested (NOT_INGESTED). */
|
||||||
409: {
|
409: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
@@ -1105,6 +1118,7 @@ export interface paths {
|
|||||||
query?: {
|
query?: {
|
||||||
includeInherited?: boolean;
|
includeInherited?: boolean;
|
||||||
kind?: string;
|
kind?: string;
|
||||||
|
sourceFile?: string;
|
||||||
};
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path: {
|
||||||
@@ -1133,7 +1147,7 @@ export interface paths {
|
|||||||
"application/json": components["schemas"]["ErrorResponse"];
|
"application/json": components["schemas"]["ErrorResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
/** @description Ambiguous module name (AMBIGUOUS_NAME, with the candidate sourceFiles in details) or an unresolved placeholder whose source is not ingested (NOT_INGESTED). */
|
||||||
409: {
|
409: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
@@ -1162,7 +1176,9 @@ export interface paths {
|
|||||||
/** Bulk Function Overrides */
|
/** Bulk Function Overrides */
|
||||||
get: {
|
get: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: {
|
||||||
|
sourceFile?: string;
|
||||||
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -1190,7 +1206,7 @@ export interface paths {
|
|||||||
"application/json": components["schemas"]["ErrorResponse"];
|
"application/json": components["schemas"]["ErrorResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
/** @description Ambiguous module name (AMBIGUOUS_NAME, with the candidate sourceFiles in details) or an unresolved placeholder whose source is not ingested (NOT_INGESTED). */
|
||||||
409: {
|
409: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
@@ -1219,7 +1235,9 @@ export interface paths {
|
|||||||
/** Function Callers */
|
/** Function Callers */
|
||||||
get: {
|
get: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: {
|
||||||
|
sourceFile?: string;
|
||||||
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path: {
|
||||||
function: string;
|
function: string;
|
||||||
@@ -1277,7 +1295,9 @@ export interface paths {
|
|||||||
/** Function Overrides */
|
/** Function Overrides */
|
||||||
get: {
|
get: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: {
|
||||||
|
sourceFile?: string;
|
||||||
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path: {
|
||||||
function: string;
|
function: string;
|
||||||
@@ -1306,7 +1326,7 @@ export interface paths {
|
|||||||
"application/json": components["schemas"]["ErrorResponse"];
|
"application/json": components["schemas"]["ErrorResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
/** @description Ambiguous module name (AMBIGUOUS_NAME, with the candidate sourceFiles in details) or an unresolved placeholder whose source is not ingested (NOT_INGESTED). */
|
||||||
409: {
|
409: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
@@ -1345,6 +1365,7 @@ export interface paths {
|
|||||||
direction?: string;
|
direction?: string;
|
||||||
/** @description Cap on the number of nodes returned (BFS order). */
|
/** @description Cap on the number of nodes returned (BFS order). */
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
sourceFile?: string;
|
||||||
};
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path: {
|
||||||
@@ -1393,7 +1414,9 @@ export interface paths {
|
|||||||
/** Payload */
|
/** Payload */
|
||||||
get: {
|
get: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: {
|
||||||
|
sourceFile?: string;
|
||||||
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -1421,7 +1444,7 @@ export interface paths {
|
|||||||
"application/json": components["schemas"]["ErrorResponse"];
|
"application/json": components["schemas"]["ErrorResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
/** @description Ambiguous module name (AMBIGUOUS_NAME, with the candidate sourceFiles in details) or an unresolved placeholder whose source is not ingested (NOT_INGESTED). */
|
||||||
409: {
|
409: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
@@ -1452,6 +1475,7 @@ export interface paths {
|
|||||||
parameters: {
|
parameters: {
|
||||||
query?: {
|
query?: {
|
||||||
endLine?: number;
|
endLine?: number;
|
||||||
|
sourceFile?: string;
|
||||||
startLine?: number;
|
startLine?: number;
|
||||||
};
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
@@ -1503,6 +1527,7 @@ export interface paths {
|
|||||||
parameters: {
|
parameters: {
|
||||||
query?: {
|
query?: {
|
||||||
depth?: number;
|
depth?: number;
|
||||||
|
sourceFile?: string;
|
||||||
};
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path: {
|
||||||
@@ -1531,7 +1556,7 @@ export interface paths {
|
|||||||
"application/json": components["schemas"]["ErrorResponse"];
|
"application/json": components["schemas"]["ErrorResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
/** @description Ambiguous module name (AMBIGUOUS_NAME, with the candidate sourceFiles in details) or an unresolved placeholder whose source is not ingested (NOT_INGESTED). */
|
||||||
409: {
|
409: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
@@ -1563,6 +1588,7 @@ export interface paths {
|
|||||||
query?: {
|
query?: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
sourceFile?: string;
|
||||||
};
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path: {
|
||||||
@@ -1591,7 +1617,7 @@ export interface paths {
|
|||||||
"application/json": components["schemas"]["ErrorResponse"];
|
"application/json": components["schemas"]["ErrorResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
/** @description Ambiguous module name (AMBIGUOUS_NAME, with the candidate sourceFiles in details) or an unresolved placeholder whose source is not ingested (NOT_INGESTED). */
|
||||||
409: {
|
409: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
@@ -2759,6 +2785,7 @@ export interface components {
|
|||||||
sloc?: number;
|
sloc?: number;
|
||||||
ingestStatus?: string;
|
ingestStatus?: string;
|
||||||
ingestDepth?: string;
|
ingestDepth?: string;
|
||||||
|
simpleName?: string;
|
||||||
};
|
};
|
||||||
NextAction: {
|
NextAction: {
|
||||||
method?: string;
|
method?: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {useCalls} from "../api/hooks";
|
import {useCalls} from "../api/hooks";
|
||||||
|
import {shortName} from "../api/names";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
project: string;
|
project: string;
|
||||||
@@ -41,7 +42,7 @@ function CallList({
|
|||||||
onClick={() => it.name && onOpen(it.name)}
|
onClick={() => it.name && onOpen(it.name)}
|
||||||
className="flex w-full items-center gap-2 rounded px-1.5 py-1 text-left text-sm hover:bg-neutral-100 dark:hover:bg-neutral-800"
|
className="flex w-full items-center gap-2 rounded px-1.5 py-1 text-left text-sm hover:bg-neutral-100 dark:hover:bg-neutral-800"
|
||||||
>
|
>
|
||||||
<span className="truncate">{it.name}</span>
|
<span className="truncate" title={it.name}>{shortName(it.name)}</span>
|
||||||
{it.edgeKind && (
|
{it.edgeKind && (
|
||||||
<span
|
<span
|
||||||
className="ml-auto shrink-0 rounded bg-neutral-100 px-1 text-[10px] text-neutral-500 dark:bg-neutral-900">
|
className="ml-auto shrink-0 rounded bg-neutral-100 px-1 text-[10px] text-neutral-500 dark:bg-neutral-900">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {useCalls} from "../api/hooks";
|
import {useCalls} from "../api/hooks";
|
||||||
|
import {shortName} from "../api/names";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
project: string;
|
project: string;
|
||||||
@@ -51,8 +52,9 @@ function TreeNode({
|
|||||||
className={`truncate rounded px-1 text-left hover:bg-neutral-100 dark:hover:bg-neutral-800 ${
|
className={`truncate rounded px-1 text-left hover:bg-neutral-100 dark:hover:bg-neutral-800 ${
|
||||||
root ? "font-semibold" : ""
|
root ? "font-semibold" : ""
|
||||||
}`}
|
}`}
|
||||||
|
title={name}
|
||||||
>
|
>
|
||||||
{name}
|
{shortName(name)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{open && (
|
{open && (
|
||||||
@@ -61,8 +63,8 @@ function TreeNode({
|
|||||||
{!isLoading && children.length === 0 && <div className="text-xs text-neutral-400">no callees</div>}
|
{!isLoading && children.length === 0 && <div className="text-xs text-neutral-400">no callees</div>}
|
||||||
{children.map((child) =>
|
{children.map((child) =>
|
||||||
ancestors.includes(child) ? (
|
ancestors.includes(child) ? (
|
||||||
<div key={child} className="ml-1 text-xs text-neutral-400" title="cycle">
|
<div key={child} className="ml-1 text-xs text-neutral-400" title={`cycle: ${child}`}>
|
||||||
↻ {child}
|
↻ {shortName(child)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<TreeNode
|
<TreeNode
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {useFieldFlow, useFlow} from "../api/hooks";
|
import {useFieldFlow, useFlow} from "../api/hooks";
|
||||||
|
import {shortName} from "../api/names";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
project: string;
|
project: string;
|
||||||
@@ -122,8 +123,9 @@ function FlowSection({project, moduleName, variable, dir, title, onOpen, onSelec
|
|||||||
{s.variableType && <span className="text-[10px] text-neutral-400">{s.variableType}</span>}
|
{s.variableType && <span className="text-[10px] text-neutral-400">{s.variableType}</span>}
|
||||||
{s.module && (
|
{s.module && (
|
||||||
<button onClick={() => onOpen(s.module!)}
|
<button onClick={() => onOpen(s.module!)}
|
||||||
|
title={s.module}
|
||||||
className="ml-auto text-neutral-500 hover:text-blue-600 dark:hover:text-blue-400">
|
className="ml-auto text-neutral-500 hover:text-blue-600 dark:hover:text-blue-400">
|
||||||
{s.module}
|
{shortName(s.module)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Graph from "graphology";
|
|||||||
import Sigma from "sigma";
|
import Sigma from "sigma";
|
||||||
import forceAtlas2 from "graphology-layout-forceatlas2";
|
import forceAtlas2 from "graphology-layout-forceatlas2";
|
||||||
import {fetchEgoGraph} from "../api/hooks";
|
import {fetchEgoGraph} from "../api/hooks";
|
||||||
|
import {shortName} from "../api/names";
|
||||||
import type {EgoGraph} from "../api/client";
|
import type {EgoGraph} from "../api/client";
|
||||||
|
|
||||||
type Direction = "in" | "out" | "both";
|
type Direction = "in" | "out" | "both";
|
||||||
@@ -32,7 +33,9 @@ function mergeEgo(graph: Graph, ego: EgoGraph, rootName: string) {
|
|||||||
if (!n.name) continue;
|
if (!n.name) continue;
|
||||||
const isRoot = n.name === rootName;
|
const isRoot = n.name === rootName;
|
||||||
graph.mergeNode(n.name, {
|
graph.mergeNode(n.name, {
|
||||||
label: n.name,
|
// Node *key* stays the identity (item 117: the FQN); only the rendered label is shortened,
|
||||||
|
// or a Java graph is a wall of `com.example.…` and the nodes size to their package path.
|
||||||
|
label: shortName(n.name),
|
||||||
sourceFile: n.sourceFile ?? "",
|
sourceFile: n.sourceFile ?? "",
|
||||||
unresolved: !!n.unresolved,
|
unresolved: !!n.unresolved,
|
||||||
color: isRoot ? COLOR.root : n.unresolved ? COLOR.unresolved : COLOR.resolved,
|
color: isRoot ? COLOR.root : n.unresolved ? COLOR.unresolved : COLOR.resolved,
|
||||||
@@ -190,7 +193,7 @@ export default function GraphView({project, moduleName, onOpen}: Props) {
|
|||||||
{selected && (
|
{selected && (
|
||||||
<div
|
<div
|
||||||
className="absolute right-2 top-2 z-10 flex items-center gap-2 rounded border border-neutral-300 bg-white/95 px-2 py-1 text-xs shadow dark:border-neutral-700 dark:bg-neutral-900/95">
|
className="absolute right-2 top-2 z-10 flex items-center gap-2 rounded border border-neutral-300 bg-white/95 px-2 py-1 text-xs shadow dark:border-neutral-700 dark:bg-neutral-900/95">
|
||||||
<span className="max-w-40 truncate font-medium">{selected}</span>
|
<span className="max-w-40 truncate font-medium" title={selected}>{shortName(selected)}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => expand(selected)}
|
onClick={() => expand(selected)}
|
||||||
className="rounded border border-neutral-300 px-1.5 py-0.5 hover:bg-neutral-100 dark:border-neutral-700 dark:hover:bg-neutral-800"
|
className="rounded border border-neutral-300 px-1.5 py-0.5 hover:bg-neutral-100 dark:border-neutral-700 dark:hover:bg-neutral-800"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {useMemo} from "react";
|
import {useMemo} from "react";
|
||||||
import {useImpactCallers} from "../api/hooks";
|
import {useImpactCallers} from "../api/hooks";
|
||||||
|
import {shortName} from "../api/names";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
project: string;
|
project: string;
|
||||||
@@ -62,14 +63,16 @@ export function ImpactView({project, moduleName, onOpen}: Props) {
|
|||||||
key={`${n.name}-${i}`}
|
key={`${n.name}-${i}`}
|
||||||
disabled={n.unresolved || !n.name}
|
disabled={n.unresolved || !n.name}
|
||||||
onClick={() => n.name && onOpen(n.name)}
|
onClick={() => n.name && onOpen(n.name)}
|
||||||
title={n.unresolved ? "unresolved dynamic-dispatch caller" : n.sourceFile}
|
title={n.unresolved
|
||||||
|
? "unresolved dynamic-dispatch caller"
|
||||||
|
: [n.name, n.sourceFile].filter(Boolean).join("\n")}
|
||||||
className={`rounded px-1.5 py-0.5 font-mono text-xs ${
|
className={`rounded px-1.5 py-0.5 font-mono text-xs ${
|
||||||
n.unresolved
|
n.unresolved
|
||||||
? "cursor-default bg-amber-100 text-amber-700 opacity-80 dark:bg-amber-900/40 dark:text-amber-300"
|
? "cursor-default bg-amber-100 text-amber-700 opacity-80 dark:bg-amber-900/40 dark:text-amber-300"
|
||||||
: "bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
: "bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{n.name}
|
{shortName(n.name)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,7 @@
|
|||||||
import {lazy, Suspense, useState} from "react";
|
import {lazy, Suspense, useState} from "react";
|
||||||
import type {ModuleInfo} from "../api/client";
|
import type {ModuleInfo} from "../api/client";
|
||||||
import {isModuleUnavailable, useIngestNeighborhood, useModuleContext, useRefreshModule} from "../api/hooks";
|
import {isModuleUnavailable, useIngestNeighborhood, useModuleContext, useRefreshModule} from "../api/hooks";
|
||||||
|
import {displayName} from "../api/names";
|
||||||
import {StatusBadge} from "./StatusBadge";
|
import {StatusBadge} from "./StatusBadge";
|
||||||
import {ModuleOverview} from "./ModuleOverview";
|
import {ModuleOverview} from "./ModuleOverview";
|
||||||
import {SourceView} from "./SourceView";
|
import {SourceView} from "./SourceView";
|
||||||
@@ -79,7 +80,9 @@ export function ModuleView({
|
|||||||
<div className="flex items-start gap-2 border-b border-neutral-200 px-4 py-3 dark:border-neutral-800">
|
<div className="flex items-start gap-2 border-b border-neutral-200 px-4 py-3 dark:border-neutral-800">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h2 className="truncate text-sm font-semibold">{module.name}</h2>
|
<h2 className="truncate text-sm font-semibold" title={module.name}>
|
||||||
|
{displayName(module.name, module.simpleName)}
|
||||||
|
</h2>
|
||||||
<StatusBadge status={module.ingestStatus} depth={module.ingestDepth}/>
|
<StatusBadge status={module.ingestStatus} depth={module.ingestDepth}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="truncate text-xs text-neutral-400">{module.sourceFile}</div>
|
<div className="truncate text-xs text-neutral-400">{module.sourceFile}</div>
|
||||||
|
|||||||
@@ -28,8 +28,13 @@ export function Explorer() {
|
|||||||
const all = data ?? [];
|
const all = data ?? [];
|
||||||
const q = search.trim();
|
const q = search.trim();
|
||||||
if (!q) return all;
|
if (!q) return all;
|
||||||
// Treat the query as a case-insensitive regex over name/sourceFile; while it's an incomplete
|
// Treat the query as a case-insensitive regex over name/simpleName/sourceFile; while it's an
|
||||||
// (invalid) pattern, fall back to a plain substring match so the list still filters as you type.
|
// incomplete (invalid) pattern, fall back to a plain substring match so the list still filters
|
||||||
|
// as you type.
|
||||||
|
//
|
||||||
|
// Item 117: `name` is a Java module's fully-qualified name, so an anchored pattern on the class
|
||||||
|
// itself (`^AbstractLogic$`) stopped matching anything. Matching `simpleName` as well restores
|
||||||
|
// that without giving up search by package or by path.
|
||||||
let re: RegExp | null = null;
|
let re: RegExp | null = null;
|
||||||
try {
|
try {
|
||||||
re = new RegExp(q, "i");
|
re = new RegExp(q, "i");
|
||||||
@@ -39,10 +44,12 @@ export function Explorer() {
|
|||||||
const lower = q.toLowerCase();
|
const lower = q.toLowerCase();
|
||||||
return all.filter((m) => {
|
return all.filter((m) => {
|
||||||
const name = m.name ?? "";
|
const name = m.name ?? "";
|
||||||
|
const simple = m.simpleName ?? "";
|
||||||
const file = m.sourceFile ?? "";
|
const file = m.sourceFile ?? "";
|
||||||
return re
|
return re
|
||||||
? re.test(name) || re.test(file)
|
? re.test(name) || re.test(simple) || re.test(file)
|
||||||
: name.toLowerCase().includes(lower) || file.toLowerCase().includes(lower);
|
: name.toLowerCase().includes(lower) || simple.toLowerCase().includes(lower)
|
||||||
|
|| file.toLowerCase().includes(lower);
|
||||||
});
|
});
|
||||||
}, [data, search]);
|
}, [data, search]);
|
||||||
|
|
||||||
@@ -167,6 +174,10 @@ export function Explorer() {
|
|||||||
<option value="">all kinds</option>
|
<option value="">all kinds</option>
|
||||||
<option value="CLASS">CLASS</option>
|
<option value="CLASS">CLASS</option>
|
||||||
<option value="INTERFACE">INTERFACE</option>
|
<option value="INTERFACE">INTERFACE</option>
|
||||||
|
{/* Item 119: enums, records and annotation types became modules. */}
|
||||||
|
<option value="ENUM">ENUM</option>
|
||||||
|
<option value="RECORD">RECORD</option>
|
||||||
|
<option value="ANNOTATION">ANNOTATION</option>
|
||||||
<option value="PROGRAM">PROGRAM</option>
|
<option value="PROGRAM">PROGRAM</option>
|
||||||
<option value="SUBPROGRAM">SUBPROGRAM</option>
|
<option value="SUBPROGRAM">SUBPROGRAM</option>
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ LoC split** surfaced by `/loc` (`userExitLoc`/`userExitSloc` vs `generatedExclus
|
|||||||
consequence: when you read source to verify an API response for a Natural module, read the
|
consequence: when you read source to verify an API response for a Natural module, read the
|
||||||
`generatedDir` copy (e.g. `generated_src/subprogram/WGEAGB0S.nat`) — the `user_exit` copy is a partial
|
`generatedDir` copy (e.g. `generated_src/subprogram/WGEAGB0S.nat`) — the `user_exit` copy is a partial
|
||||||
fragment and does not represent what was analysed. See the item-47 split in
|
fragment and does not represent what was analysed. See the item-47 split in
|
||||||
`mcp-api-usage-ac-implementation.md`.
|
`agent-api-usage-ac-implementation.md`.
|
||||||
|
|
||||||
## 1. Pick a project
|
## 1. Pick a project
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,18 @@ GET /modules/OrderService/digest → 200 when unique, else 409 AMB
|
|||||||
Responses carry `simpleName` alongside `name` for display. The `?module=` and `?extends=` filters and
|
Responses carry `simpleName` alongside `name` for display. The `?module=` and `?extends=` filters and
|
||||||
the `ac` CLI take either form too; `--source-file` remains available on every module command.
|
the `ac` CLI take either form too; `--source-file` remains available on every module command.
|
||||||
|
|
||||||
|
**Which Java types are modules** (item 119). Classes, interfaces, **enums, records and annotation
|
||||||
|
types** — `moduleKind` is one of `CLASS | INTERFACE | ENUM | RECORD | ANNOTATION` (Natural adds
|
||||||
|
`PROGRAM | SUBPROGRAM | …`), and `?moduleKind=` filters on it. Their content is modelled the way each
|
||||||
|
kind carries it: a record's components and an annotation type's members are `FIELD`s (the latter with
|
||||||
|
`defaultValue` where declared), an enum's constants are `CONSTANT`s, and an enum's or record's
|
||||||
|
`implements` is a real edge — so a call against an interface fans out to an enum implementing it.
|
||||||
|
|
||||||
|
Before 119 these three kinds were not parsed at all: `GET /modules/SomeEnum/digest` answered `404`,
|
||||||
|
and a record referenced from elsewhere stayed an unresolved placeholder (`409 NOT_INGESTED`). One
|
||||||
|
gap remains by design — a record's *compact* canonical constructor is not a function node, so calls
|
||||||
|
made in its body are invisible.
|
||||||
|
|
||||||
Natural is unaffected throughout: its module names are file stems, and colliding identities are
|
Natural is unaffected throughout: its module names are file stems, and colliding identities are
|
||||||
skipped at ingest, so they are unique by construction (`upms` has zero ambiguous names, `pur` 163).
|
skipped at ingest, so they are unique by construction (`upms` has zero ambiguous names, `pur` 163).
|
||||||
|
|
||||||
@@ -4,6 +4,14 @@ Completed work, moved out of `x-docs/roadmap.md` (which now tracks only open
|
|||||||
items). Each entry records what was built; IDs are preserved from the roadmap
|
items). Each entry records what was built; IDs are preserved from the roadmap
|
||||||
(some IDs recur across sections — they are kept as-is for traceability).
|
(some IDs recur across sections — they are kept as-is for traceability).
|
||||||
|
|
||||||
|
> **The MCP surface no longer exists.** It was removed on 2026-08-04 (roadmap item 26) after
|
||||||
|
> intermittent session failures that were not fixable from this codebase; all 40 tools had a REST
|
||||||
|
> twin, so no capability was lost. **REST and the `ac` CLI are the only access paths.**
|
||||||
|
>
|
||||||
|
> Entries below still name MCP tools (`module_payload`, `McpQueryTools`, …). Those are kept as the
|
||||||
|
> historical record of what each feature shipped with — they are **not** a description of anything
|
||||||
|
> callable today. Where an entry reads as present tense, read "REST + CLI".
|
||||||
|
|
||||||
## Metrics
|
## Metrics
|
||||||
|
|
||||||
- [x] **47. Generated vs. user-exit LoC/SLoC split** (2026-07-13) — a project can declare a source
|
- [x] **47. Generated vs. user-exit LoC/SLoC split** (2026-07-13) — a project can declare a source
|
||||||
@@ -364,7 +372,7 @@ automatic invalidation, remains open in the roadmap.)
|
|||||||
Natural **catalog metadata**, and `main-program`/`external-subprogram`/
|
Natural **catalog metadata**, and `main-program`/`external-subprogram`/
|
||||||
`subprogram` are syntactically identical productions — no reliable
|
`subprogram` are syntactically identical productions — no reliable
|
||||||
source-level signal exists for these without the original catalog. See
|
source-level signal exists for these without the original catalog. See
|
||||||
`x-docs/mcp-api-usage-ac-implementation.md` step 0 for the heuristic's limits.
|
`x-docs/agent-api-usage-ac-implementation.md` step 0 for the heuristic's limits.
|
||||||
|
|
||||||
## Foundation (schema, health, errors, ingest CLI)
|
## Foundation (schema, health, errors, ingest CLI)
|
||||||
|
|
||||||
@@ -960,7 +968,7 @@ automatic invalidation, remains open in the roadmap.)
|
|||||||
property, tagged on the existing call-site candidate); and a no-generic-entity
|
property, tagged on the existing call-site candidate); and a no-generic-entity
|
||||||
fallback (repository interfaces with no resolvable generic type anywhere, e.g. a
|
fallback (repository interfaces with no resolvable generic type anywhere, e.g. a
|
||||||
custom `IRiskRepository`, guess the entity from a declared method's return type).
|
custom `IRiskRepository`, guess the entity from a declared method's return type).
|
||||||
See `x-docs/mcp-api-usage-ac-implementation.md` for semantics/limits.
|
See `x-docs/agent-api-usage-ac-implementation.md` for semantics/limits.
|
||||||
|
|
||||||
## Project model & ingest endpoints
|
## Project model & ingest endpoints
|
||||||
|
|
||||||
@@ -1586,7 +1594,7 @@ question the graph already had the data for.
|
|||||||
matching annotation, backed by a new generic `annotations` property captured
|
matching annotation, backed by a new generic `annotations` property captured
|
||||||
at parse time on every such node (independent of any annotation's own specific
|
at parse time on every such node (independent of any annotation's own specific
|
||||||
interpretation elsewhere, e.g. `@Entity`/`@Query`). See
|
interpretation elsewhere, e.g. `@Entity`/`@Query`). See
|
||||||
`x-docs/mcp-api-usage-ac-implementation.md` section 7-8 for usage.
|
`x-docs/agent-api-usage-ac-implementation.md` section 7-8 for usage.
|
||||||
|
|
||||||
- [x] **31. Resolve inherited `REFERENCES`/wiring edges on concrete subclasses**
|
- [x] **31. Resolve inherited `REFERENCES`/wiring edges on concrete subclasses**
|
||||||
(found 2026-07-07/08, PUR `pur-batch` re-evaluation, done 2026-07-09) — `call_tree`/
|
(found 2026-07-07/08, PUR `pur-batch` re-evaluation, done 2026-07-09) — `call_tree`/
|
||||||
@@ -1701,7 +1709,7 @@ question the graph already had the data for.
|
|||||||
the validation can't drift between transports. New `McpToolsIT` drives the whole
|
the validation can't drift between transports. New `McpToolsIT` drives the whole
|
||||||
pipeline through tools only (connect → `ingest_all` → `list_modules`/`list_projects`
|
pipeline through tools only (connect → `ingest_all` → `list_modules`/`list_projects`
|
||||||
+ a `PROJECT_NOT_FOUND` error case); all 58 REST ITs still green after the
|
+ a `PROJECT_NOT_FOUND` error case); all 58 REST ITs still green after the
|
||||||
resolver refactor. Docs: `mcp-api-usage-ac-implementation.md` gained an "Access via MCP"
|
resolver refactor. Docs: `agent-api-usage-ac-implementation.md` gained an "Access via MCP"
|
||||||
section.
|
section.
|
||||||
|
|
||||||
- [x] **30. Version number in startup log / API / MCP** (done 2026-07-08) —
|
- [x] **30. Version number in startup log / API / MCP** (done 2026-07-08) —
|
||||||
@@ -2242,7 +2250,7 @@ its own; `McpSupport` only serialized service results into the same JSON the RES
|
|||||||
**Docs & rules updated.** `CLAUDE.md` (sync rule now REST + `ac-cli`; tool priority now REST → CLI →
|
**Docs & rules updated.** `CLAUDE.md` (sync rule now REST + `ac-cli`; tool priority now REST → CLI →
|
||||||
grep/Explore; architecture diagram, project structure, ADR table), `README.md` (architecture diagram,
|
grep/Explore; architecture diagram, project structure, ADR table), `README.md` (architecture diagram,
|
||||||
tech stack, endpoint list, "Agent usage" section replacing "MCP"), `x-docs/agent-api-system-prompt.md`
|
tech stack, endpoint list, "Agent usage" section replacing "MCP"), `x-docs/agent-api-system-prompt.md`
|
||||||
("Access via MCP" section and its REST↔tool mapping table dropped), `x-docs/mcp-api-usage-ac-implementation.md`
|
("Access via MCP" section and its REST↔tool mapping table dropped), `x-docs/agent-api-usage-ac-implementation.md`
|
||||||
(tool names replaced by their endpoint paths throughout; filename kept to avoid breaking ~6 inbound
|
(tool names replaced by their endpoint paths throughout; filename kept to avoid breaking ~6 inbound
|
||||||
references), `x-docs/agenticcode-ueberblick.md`, `x-docs/presentation.md`, `prompts/CLAUDE.md`, both
|
references), `x-docs/agenticcode-ueberblick.md`, `x-docs/presentation.md`, `prompts/CLAUDE.md`, both
|
||||||
`prompts/*-deep-api-audit.md`, and `.claude/settings.local.json`. Historical MCP mentions in this file
|
`prompts/*-deep-api-audit.md`, and `.claude/settings.local.json`. Historical MCP mentions in this file
|
||||||
|
|||||||
@@ -321,6 +321,91 @@ before. The parked "parallel parse phase" idea was implemented 2026-07-18 (item
|
|||||||
|
|
||||||
## Known bugs
|
## Known bugs
|
||||||
|
|
||||||
|
- [x] **119. Enums, records and annotation types are modules** (found 2026-08-06 in the `pur`
|
||||||
|
source-vs-API cross-check; **done 2026-08-06**)
|
||||||
|
|
||||||
|
The Java parser iterated `ClassOrInterfaceDeclaration`. Everything else was invisible — in `pur`
|
||||||
|
**76** annotation types, **67** enums and **25** records, 168 types with no `MODULE` node at all
|
||||||
|
(plus 384 `package-info.java`, correctly ignored). `GET /modules/BatchParam/digest` answered `404`;
|
||||||
|
a record referenced from another file stayed an unresolved placeholder and answered
|
||||||
|
`409 NOT_INGESTED`. The API did not lie — item 107 saw to that — but a whole category of type was
|
||||||
|
unanalysable.
|
||||||
|
|
||||||
|
The loop now iterates `TypeDeclaration`. Exactly three things differ between the four kinds, and
|
||||||
|
they are resolved once in a `TypeFacts` adapter instead of scattering `instanceof` through a
|
||||||
|
250-line body: only a class/interface can `extends`, only an annotation type can do neither, and
|
||||||
|
`isInterface` is a class/interface notion. `moduleKind` gains `ENUM | RECORD | ANNOTATION`.
|
||||||
|
|
||||||
|
**What the probe corrected in the plan.** Rather than reason about the JavaParser API I ran a
|
||||||
|
parser over a fixture with all four kinds, and two assumptions were wrong:
|
||||||
|
- A record's *compact* canonical constructor (`Point { … }`) is **not** returned by
|
||||||
|
`getConstructors()` — `CompactConstructorDeclaration` does not extend `CallableDeclaration` and so
|
||||||
|
does not fit the callable machinery. Left uncovered deliberately (1 occurrence in `pur`), and
|
||||||
|
named here because the calls in its body stay invisible.
|
||||||
|
- An annotation type's `String value();` is **not** a method — `getMethods()` returns none. Building
|
||||||
|
it as planned would have given 76 annotation types a module node with an empty body: "analysed,
|
||||||
|
nothing found" again. Members are modelled as `FIELD`s with `defaultValue`, because the question
|
||||||
|
asked of an annotation is which attributes it carries.
|
||||||
|
|
||||||
|
Record components and enum constants are captured for the same reason — a record DTO reporting zero
|
||||||
|
fields is worse than no answer. The `TypeResolver` and the enclosing-type walk had to follow, or a
|
||||||
|
reference to an enum declared in the same file would have been unqualifiable — item 117 running
|
||||||
|
backwards for precisely the types it just gained.
|
||||||
|
|
||||||
|
Kept class/interface-only on purpose: the JPA/Panache repository heuristics. A record is not a
|
||||||
|
Panache repository, and generalizing guesswork to types it was never written for produces confident
|
||||||
|
wrong answers rather than silence.
|
||||||
|
|
||||||
|
**Watched, not asserted:** the CHA fan-out grows, since an enum implementing a project interface is
|
||||||
|
now a real implementation target. And 168 additional types can make a previously unique
|
||||||
|
`simpleName` ambiguous, so a short name that used to work may now answer `409` — the honest answer
|
||||||
|
under 115, but a visible change.
|
||||||
|
|
||||||
|
- [x] **118b. A control character in one constant disabled two guards, and the tests that should have
|
||||||
|
caught it asserted the same wrong literal** (found 2026-08-06 in the `pur` cross-check;
|
||||||
|
**done 2026-08-06**; both regressions from 116b)
|
||||||
|
|
||||||
|
**A — the marker leak.** `UNRESOLVED_FIELD_RECEIVER_PREFIX` contained a stray `U+0001`:
|
||||||
|
|
||||||
|
```
|
||||||
|
JavaParser.java:157 = ".field:";
|
||||||
|
hex: 3d 20 22 01 66 69 65 6c 64 3a 22 3b
|
||||||
|
```
|
||||||
|
|
||||||
|
So the parser wrote `field:x` while both Cypher guards matched `STARTS WITH 'field:'` — the
|
||||||
|
cleanup in `DELETE_UNRESOLVED_FIELD_RECEIVERS` and the exclusion in
|
||||||
|
`RESOLVE_SIMPLE_NAME_REFERENCES`. Neither ever matched. In `pur`, 202 marker nodes survived, 180 of
|
||||||
|
them still wired with 850 edges, and the scaffolding was served from the public API:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /modules/…KundeService/callees → { "name": "field:e", … }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why it survived review and tests.** The two ITs written to guard exactly this checked
|
||||||
|
`m.name STARTS WITH 'field:'` and `not hasItem("field:repo")` — the same wrong literal. They were
|
||||||
|
true because they matched nothing. Code and test were wrong in the same way, so the test could not
|
||||||
|
see the bug it existed for.
|
||||||
|
|
||||||
|
Fixed at the source, and both predicates now match `CONTAINS 'field:'`: a ':' cannot occur in a Java
|
||||||
|
or Natural module name (verified across all four projects), so it is equally sharp — and it also
|
||||||
|
clears markers left by an earlier build, which a refresh would never reach otherwise (placeholders
|
||||||
|
have no `sourceFile`, so the per-file sweeps do not touch them).
|
||||||
|
|
||||||
|
**B — lambda parameters taken for inherited fields.** `isProbableFieldReceiver` asked "lower-case
|
||||||
|
and not in `declaredTypes`?". Lambda and `catch` parameters are in neither the callable's parameter
|
||||||
|
list nor its `VariableDeclarationExpr`s, so `.map(e -> e.getX())` looked like an inherited field
|
||||||
|
named `e` — 24 markers from one class alone, none of them ever resolvable. The bound names now go
|
||||||
|
into the existing `shadowed` set. Deliberately *not* into `declaredTypes`: an implicit lambda
|
||||||
|
parameter has type `UnknownType`, and feeding that to the receiver resolver would turn a silent
|
||||||
|
omission into a confident edge to a module named after a non-type.
|
||||||
|
|
||||||
|
**Both fixes proven by reverting them.** With A restored to its broken form, three assertions fail —
|
||||||
|
including `field:somethingUndeclared` reaching `/callees`. The first B test I wrote was *itself*
|
||||||
|
vacuous: with A fixed the cleanup deletes every marker, so nothing is observable end-to-end. It moved
|
||||||
|
to `JavaParserTest`, where reverting B turns it red with `[field:entry, field:ex, field:inherited]`.
|
||||||
|
The new assertions match the marker anywhere in the name and, separately, assert the invariant that
|
||||||
|
was actually violated — no module name contains a control character.
|
||||||
|
|
||||||
- [x] **118. The Java DB resolvers scanned the project once per candidate row** (found 2026-08-06
|
- [x] **118. The Java DB resolvers scanned the project once per candidate row** (found 2026-08-06
|
||||||
while watching a `pur` refresh; **done 2026-08-06**; regression from [117])
|
while watching a `pur` refresh; **done 2026-08-06**; regression from [117])
|
||||||
|
|
||||||
@@ -417,9 +502,44 @@ before. The parked "parallel parse phase" idea was implemented 2026-07-18 (item
|
|||||||
Also unified on the way: `declaredIn` is a display label in every producer (column metadata and both
|
Also unified on the way: `declaredIn` is a display label in every producer (column metadata and both
|
||||||
function queries), i.e. the short name. Two of the three had drifted to the identity.
|
function queries), i.e. the short name. Two of the three had drifted to the identity.
|
||||||
|
|
||||||
**Not done:** the web UI. Its TypeScript client is generated from the running server's OpenAPI, so
|
**UI (done 2026-08-06, after the deploy that unblocked the codegen).** The client is generated from
|
||||||
`simpleName` cannot be surfaced until a deploy exists to generate against. The CLI needs no change —
|
the running server's OpenAPI, so this had to wait for a build that knows `simpleName`. Regenerating
|
||||||
it prints the response verbatim.
|
pulled in exactly the 115/117 contract and nothing else: 18 `sourceFile` query params, 11 reworded
|
||||||
|
409 descriptions, 1 `simpleName`.
|
||||||
|
|
||||||
|
The find was that this is **not** a cosmetic task. `Explorer.tsx` filters the module list by regex
|
||||||
|
over `name`, and `name` is now the FQN — so an anchored pattern on the class itself returned
|
||||||
|
nothing:
|
||||||
|
|
||||||
|
```
|
||||||
|
^AbstractLogic$ → 0 Treffer (name = com.uniqagroup.common.base.AbstractLogic)
|
||||||
|
```
|
||||||
|
|
||||||
|
The existing explorer spec runs against `upms` (Natural, no dots in any name) and stayed green
|
||||||
|
throughout. The filter now also matches `simpleName`; the package path and the FQN remain
|
||||||
|
searchable.
|
||||||
|
|
||||||
|
Display: eight sites render the short name with the identity in the `title` — module list, module
|
||||||
|
header, graph node labels **and** the selected-node chip, callers/callees, call tree (including its
|
||||||
|
cycle markers), impact list, dataflow steps. Identity is untouched everywhere it matters: routing,
|
||||||
|
query keys, `key=`, requests, and the graphology node key. Where the server sends `simpleName` it
|
||||||
|
wins over the client-side rule, because it knows the cases a string rule cannot — a class in the
|
||||||
|
default package, or a local class for which JavaParser reports no qualified name.
|
||||||
|
|
||||||
|
The derivation ("text after the last dot") was checked against the server rather than assumed: for
|
||||||
|
all **4734** qualified modules of `pur`, it equals `simpleName` — 0 divergences; and 0 of 3587
|
||||||
|
`upms` module names contain a dot, so Natural is provably unaffected. Worth noting the graph is
|
||||||
|
currently mixed-generation (only `pur` is re-ingested post-117), which is what makes the
|
||||||
|
prefer-then-derive fallback necessary rather than nice.
|
||||||
|
|
||||||
|
**Accepted loss:** a nested class shows as `Inner`, so two `Outer.Inner` in different outers look
|
||||||
|
alike in a list. The FQN is one hover away and the list also shows the source file.
|
||||||
|
|
||||||
|
New e2e spec `java-fqn.spec.ts` (the only one that runs against a Java project) — and it was
|
||||||
|
verified to actually catch the regression by reverting the filter fix and watching it go red.
|
||||||
|
`tsc --noEmit` + `vite build` clean; Playwright 21 passed, 1 pre-existing failure in
|
||||||
|
`identifier-popover.spec.ts` (expects `line=323`, gets `324` — reproduced with all UI changes
|
||||||
|
stashed, so it predates this work and belongs to the upms data, not the UI).
|
||||||
|
|
||||||
- [x] **115. Module endpoints address Java classes by simple name and silently merge distinct
|
- [x] **115. Module endpoints address Java classes by simple name and silently merge distinct
|
||||||
classes that share one** (found 2026-08-06, `pur` source-vs-API cross-check; **done 2026-08-06**)
|
classes that share one** (found 2026-08-06, `pur` source-vs-API cross-check; **done 2026-08-06**)
|
||||||
@@ -1115,7 +1235,7 @@ wrong answer, found by the 2026-07-17 `VMULTMN4` audit.)*
|
|||||||
(genuine incoming CALLNAT/inheritance only). `context`/`digest` (both call `callers(…, null)`)
|
(genuine incoming CALLNAT/inheritance only). `context`/`digest` (both call `callers(…, null)`)
|
||||||
inherit the clean view; `scope=internal` still exposes function→function PERFORM wiring; callees
|
inherit the clean view; `scope=internal` still exposes function→function PERFORM wiring; callees
|
||||||
unchanged. Covered by `ModuleCallersSelfLoopIT` (fails 2/3 before the fix). MCP `callers` tool
|
unchanged. Covered by `ModuleCallersSelfLoopIT` (fails 2/3 before the fix). MCP `callers` tool
|
||||||
description + REST endpoint doc + `mcp-api-usage-ac-implementation.md` updated.
|
description + REST endpoint doc + `agent-api-usage-ac-implementation.md` updated.
|
||||||
*(Supersedes the earlier "Not a bug (verified): `context.callers` includes internal PERFORM callers —
|
*(Supersedes the earlier "Not a bug (verified): `context.callers` includes internal PERFORM callers —
|
||||||
noisy but accurate" note.)* Ego-graph `direction=in` for `WGEAGB0S` now returns its dynamic callers
|
noisy but accurate" note.)* Ego-graph `direction=in` for `WGEAGB0S` now returns its dynamic callers
|
||||||
`W-LST-N0`/`W-MNT-N0` (item 75). Payload `direction` is always `REQUEST` for PDA-derived contracts (a
|
`W-LST-N0`/`W-MNT-N0` (item 75). Payload `direction` is always `REQUEST` for PDA-derived contracts (a
|
||||||
@@ -1514,7 +1634,7 @@ lands in both ingest tiers at once.)*
|
|||||||
(`substrPos IS NULL`). Precedence honoured by guarding the fold against `:DynamicCallOverride` sites
|
(`substrPos IS NULL`). Precedence honoured by guarding the fold against `:DynamicCallOverride` sites
|
||||||
plus a `delete-folded-overridden-dynamic-callnat` step before `apply-manual`. Characterization ITs in
|
plus a `delete-folded-overridden-dynamic-callnat` step before `apply-manual`. Characterization ITs in
|
||||||
`DynamicCallnatFoldIT` (fold resolves `YABALKEY`+`GN0@6/3` → `YABALGN0`; manual override wins);
|
`DynamicCallnatFoldIT` (fold resolves `YABALKEY`+`GN0@6/3` → `YABALGN0`; manual override wins);
|
||||||
full dynamic-callnat regression 89/89 green. Docs in `mcp-api-usage-ac-implementation.md`.
|
full dynamic-callnat regression 89/89 green. Docs in `agent-api-usage-ac-implementation.md`.
|
||||||
|
|
||||||
- [x] **26. MCP session reliability — RESOLVED BY REMOVAL (2026-08-04)** (investigated 2026-07-07,
|
- [x] **26. MCP session reliability — RESOLVED BY REMOVAL (2026-08-04)** (investigated 2026-07-07,
|
||||||
reproduced 2026-08-02, never fixed). `mcp__agenticcode__*` calls intermittently — and in the
|
reproduced 2026-08-02, never fixed). `mcp__agenticcode__*` calls intermittently — and in the
|
||||||
|
|||||||
Reference in New Issue
Block a user