Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,14 +242,25 @@ the same repository lands on the same nodes instead of a per-language duplicate.
`SCHEMA_VERSION` is stamped onto the `:JApplication` node of every emitted graph.

Every node id starts with `can://<app>/` — `can://<app>/java/…` for code, `can://<app>/artifact/…`
for build manifests and config keys — so `can://<app>` is a prefix of every node the application
emits, which is what the destructive statements scope on. (`:Package` is the exception: it is keyed
on a `pkg:` purl, sits under no application, and no wipe reaches it.) `:JApplication` is keyed on
that id, not on the free-text `--app-name`, so the root is addressable by the same id its
descendants are prefixed with. The id is derived from `--app-name` (`can://<app-name>`), so it does
for build manifests and config keys, `can://<app>/@external/…` for library symbols — so
`can://<app>` is a prefix of every node the application emits, which is what the destructive
statements scope on. (`:Package` is the exception: it is keyed on a `pkg:` purl, sits under no
application, and no wipe reaches it.) `:JApplication` is keyed on that id, not on the free-text
`--app-name`, so the root is addressable by the same id its descendants are prefixed with. The id
is derived from `--app-name` (`can://<app-name>`), so it does
**not** disambiguate two services analyzed under the same name — those still merge onto one root.
Give each service its own `--app-name` if they share a database.

**Polyglot ids.** The `/java/` segment is what makes a code node this analyzer's. Three id families
deliberately omit it and are therefore **language-neutral, shared merge targets**: `:Artifact`,
`:ConfigKey` and `@external` symbols. A sibling analyzer over the same `<app>` mints byte-identical
ids for them, so a `pom.xml`, a config key or `java.util.Map#get` is *one* node in a merged graph
rather than a per-language duplicate. That sharing is the point, and its cost is accepted: two
analyzers' notions of a library symbol are not necessarily the same thing, and merging them says
they are. The consequence for writes: because these shared nodes sit inside `can://<app>/`, a Cypher
snapshot's prefix wipe rebuilds them, so a cross-language edge into a shared `:Artifact` is dropped
by one analyzer's snapshot and restored on the other's next push.

**Configuration reads.** `DEFINES_CONFIG` says which artifact declares a key; `J_USES_CONFIG` says
which code reads one. Its source is whichever node the read was attributed to — a `:JBodyNode` for
a call site (`System.getenv("X")`, `env.getProperty("X")`), or the `:JField` / `:JCallable` /
Expand Down
21 changes: 16 additions & 5 deletions src/main/java/com/ibm/cldk/schema/CanId.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,24 @@ public static String ordinalId(String callableId, String tag) {
}

/**
* {@code can://<app>/java/@external/<binary-type>/<signature>} — a callable outside the project.
* Positionally parallel to an in-project callable id with {@code @external} in the file slot (D19);
* the type is a <em>binary</em> name ({@code java.util.Map$Entry}) so the id is unambiguous and
* joins WALA natively.
* {@code can://<app>/@external/<binary-type>/<signature>} — a callable outside the project. The
* type is a <em>binary</em> name ({@code java.util.Map$Entry}) so the id is unambiguous and joins
* WALA natively.
*
* <p>Language-NEUTRAL, like {@link #artifactId}: {@code @external} sits in the position the
* language occupies for code nodes, so sibling analyzers over the same {@code <app>} name a
* library symbol identically and it is one node in a merged graph. The cost is real and was
* accepted deliberately — two analyzers' notions of {@code java.util.Map#get} are not necessarily
* the same thing, and merging them says they are. codeanalyzer-typescript's form;
* codeanalyzer-python follows it, and so does this.
*
* <p>Consequently an external id does <em>not</em> match the {@code canId} shape
* ({@code ^can://[^/]+/java/}): the analysis schema's {@code externalCanId} is a separate
* definition, and the two sites that hold either — a call edge's {@code dst} and a body node's
* {@code callee} — {@code $ref} the union, not a widened {@code canId}.
*/
public static String externalId(String appName, String binaryType, String signature) {
return applicationId(appName) + "/" + LANG + "/@external/" + binaryType + "/" + signature;
return applicationId(appName) + "/@external/" + binaryType + "/" + signature;
}

/**
Expand Down
6 changes: 4 additions & 2 deletions src/test/java/com/ibm/cldk/schema/CanIdTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@ void ordinalId_appendsTagAfterAt() {
}

@Test
void externalIdIsAppThenLanguage() {
void externalIdIsLanguageNeutralUnderTheApp() {
// No `/java/` segment: @external sits where the language sits for code nodes, so a sibling
// analyzer over the same app mints the same id and merges onto the same node (#244).
assertEquals(
"can://daytrader8/java/@external/java.util.Map/get(java.lang.Object)",
"can://daytrader8/@external/java.util.Map/get(java.lang.Object)",
CanId.externalId("daytrader8", "java.util.Map", "get(java.lang.Object)"));
}

Expand Down
13 changes: 9 additions & 4 deletions src/test/resources/schema/analysis.v2.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@
"required": ["src", "dst", "prov", "weight"],
"properties": {
"src": { "$ref": "#/$defs/canId" },
"dst": { "$ref": "#/$defs/canId" },
"dst": { "$ref": "#/$defs/codeOrExternalCanId" },
"prov": {
"type": "array",
"items": { "enum": ["declared", "rta"] },
Expand Down Expand Up @@ -250,8 +250,8 @@

"externalCanId": {
"type": "string",
"pattern": "^can://[^/]+/java/@external/",
"description": "Id of a symbol outside the project, so an in-project id landing in the external map fails the gate instead of validating happily."
"pattern": "^can://[^/]+/@external/",
"description": "Id of a symbol outside the project, so an in-project id landing in the external map fails the gate instead of validating happily. Language-NEUTRAL like artifactCanId: @external sits where the language segment sits for code nodes, so sibling analyzers over the same <app> name a library symbol identically. It therefore does NOT match canId."
},

"externalSymbol": {
Expand All @@ -269,6 +269,11 @@
}
},

"codeOrExternalCanId": {
"anyOf": [{ "$ref": "#/$defs/canId" }, { "$ref": "#/$defs/externalCanId" }],
"description": "A call target: an in-project canId, or an externalCanId when --external-calls homed it out of project. externalCanId is language-neutral and so does not match canId; this union is the fix, rather than loosening canId to admit ids that are not this analyzer's."
},

"artifactCanId": {
"type": "string",
"pattern": "^can://[^/]+/artifact/",
Expand Down Expand Up @@ -514,7 +519,7 @@
"span": { "$ref": "#/$defs/span" },
"parent": { "$ref": "#/$defs/localId" },
"of": { "type": "string" },
"callee": { "$ref": "#/$defs/canId" },
"callee": { "$ref": "#/$defs/codeOrExternalCanId" },
"arguments": { "type": "array", "items": { "$ref": "#/$defs/localId" } },
"receiver_expr": { "type": "string" },
"receiver_type": { "type": "string" },
Expand Down