codeanalyzer-python 1.4.1__py3-none-any.whl → 1.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
codeanalyzer/core.py CHANGED
@@ -19,6 +19,7 @@ from codeanalyzer.schema import (
19
19
  model_validate_json,
20
20
  )
21
21
  from codeanalyzer.schema.assign_ids import assign_ids
22
+ from codeanalyzer.schema.ids import external_id
22
23
  from codeanalyzer.schema.l1_body import populate_l1_body
23
24
  from codeanalyzer.schema.l2_callees import backfill_callees
24
25
  from codeanalyzer.schema.call_graph_ids import reidentify_call_graph
@@ -564,8 +565,7 @@ class Codeanalyzer:
564
565
  if sig in sig_to_id:
565
566
  continue
566
567
  module, name = sig.rsplit(".", 1) if "." in sig else (None, sig)
567
- ext_id = f"{app_id}/@external/{module}/{name}" if module else \
568
- f"{app_id}/@external/{name}"
568
+ ext_id = external_id(app_id, module, name)
569
569
  sig_to_id[sig] = ext_id
570
570
  externals[ext_id] = PyExternalSymbol(
571
571
  id=ext_id, name=name, module=module
@@ -565,6 +565,7 @@ def emit_l4(
565
565
  edge = ParamEdge(
566
566
  src=src_im.global_id(e.source_node),
567
567
  dst=dst_im.global_id(e.target_node),
568
+ var=e.var,
568
569
  )
569
570
  (app.param_in if e.type == "PARAM_IN" else app.param_out).append(edge)
570
571
 
@@ -41,8 +41,8 @@ Nodes are MERGE-upserted, never blindly deleted, so a declaration another
41
41
  MERGE-only.
42
42
 
43
43
  **Every destructive statement is scoped on the ``can://`` id prefix** (#173). The id is a
44
- path — ``can://python/<app>/<file>/...`` — so ``id = <module-id> OR id STARTS WITH
45
- <module-id> + '/'`` is containment, and it is one language, one application and one
44
+ path — ``can://<app>/python/<file>/...`` — so ``id = <module-id> OR id STARTS WITH
45
+ <module-id> + '/'`` is containment, and it is one application, one language and one
46
46
  module at once. That is what neither a label anchor nor the retired ``_module`` property
47
47
  could give: two python applications sharing ``src/foo.py`` carry identical labels and an
48
48
  identical file key, and only the id tells them apart. ``:PyCanNode`` anchors the predicate
@@ -115,10 +115,16 @@ def bolt_writer(rows: GraphRows, cfg: BoltConfig, full_run: bool, eager: bool =
115
115
  s.run(stmt)
116
116
 
117
117
  # The application anchor. Every destructive statement below is scoped to
118
- # ``can://python/<app>/``; an empty application id is refused up front rather
119
- # than becoming ``STARTS WITH ''`` (every node in the database).
118
+ # ``can://<app>/``; an empty application is refused up front rather than
119
+ # becoming ``STARTS WITH ''`` (every node in the database). The root row is
120
+ # keyed on its ``can://`` id now, so the name comes off its props — reading
121
+ # ``n.value`` here would build ``can://can://<app>/``.
120
122
  app_name = next(
121
- (n.value for n in rows.nodes if n.labels and n.labels[0] == "PyApplication"),
123
+ (
124
+ n.props.get("name")
125
+ for n in rows.nodes
126
+ if n.labels and n.labels[0] == "PyApplication"
127
+ ),
122
128
  None,
123
129
  )
124
130
  app_prefix = application_prefix(app_name)
@@ -195,7 +201,7 @@ def bolt_writer(rows: GraphRows, cfg: BoltConfig, full_run: bool, eager: bool =
195
201
  _upsert_edges(session, neo4j, edges)
196
202
 
197
203
  # 6. orphan prune — only safe on a full run (a targeted run can't tell deleted from untargeted).
198
- # Scoped to ``can://python/<app>/`` so a full run for application B never deletes
204
+ # Scoped to ``can://<app>/`` so a full run for application B never deletes
199
205
  # application A's modules from a shared database — even when both are python and
200
206
  # share a module path.
201
207
  if full_run and eager:
@@ -38,6 +38,7 @@ from codeanalyzer.neo4j.rows import (
38
38
  cypher_value,
39
39
  )
40
40
  from codeanalyzer.neo4j.schema import CONSTRAINTS, INDEXES
41
+ from codeanalyzer.schema.ids import application_id
41
42
 
42
43
  BATCH = 500
43
44
 
@@ -68,17 +69,24 @@ def render_cypher(rows: GraphRows, app_name: str) -> str:
68
69
 
69
70
 
70
71
  def _wipe(app_name: str) -> str:
71
- """Everything under ``can://python/<app>/`` plus the application anchor (#173).
72
- Scoped by id prefix, so it is one language and one application by construction —
73
- a second python app sharing a module path, a sibling analyzer's graph, and the
74
- cross-language :Artifact / :Package nodes are all outside it."""
72
+ """The application root by equality plus everything under ``can://<app>/`` (#173).
73
+ Scoped by id prefix, so it is one application by construction — a second python
74
+ app sharing a module path, and a sibling analyzer's :Py* graph, are outside it.
75
+ :Package nodes (``pkg:`` purls) stay outside too.
76
+
77
+ Two things moved with the app-outermost grammar. The root is matched by its
78
+ ``can://<app>`` id rather than by the free-text ``--app-name``, so two apps
79
+ sharing a name no longer wipe each other's root; and the app's :Artifact /
80
+ :ConfigKey nodes are now *inside* the prefix, so the snapshot rebuilds them
81
+ instead of leaving them to accumulate. That is deliberate, and it is the one
82
+ behavioural widening here: a cross-language edge into a shared :Artifact is
83
+ dropped by a python snapshot and restored on that analyzer's next push."""
75
84
  prefix = cypher_value(application_prefix(app_name))
76
- name = cypher_value(app_name)
85
+ app_id = cypher_value(application_id(app_name))
77
86
  return "\n".join(
78
87
  [
79
- f"MATCH (x:{CAN_NODE}) WHERE x.id STARTS WITH {prefix}",
88
+ f"MATCH (x:{CAN_NODE}) WHERE x.id = {app_id} OR x.id STARTS WITH {prefix}",
80
89
  "CALL { WITH x DETACH DELETE x } IN TRANSACTIONS OF 1000 ROWS;",
81
- f"MATCH (a:PyApplication {{name: {name}}}) DETACH DELETE a;",
82
90
  ]
83
91
  )
84
92
 
@@ -49,7 +49,7 @@ from codeanalyzer.schema import (
49
49
  PyVariableDeclaration,
50
50
  )
51
51
  from codeanalyzer.schema import model_dump
52
- from codeanalyzer.schema.ids import application_id, global_ordinal, purl_pypi
52
+ from codeanalyzer.schema.ids import application_id, external_id, global_ordinal, purl_pypi
53
53
  from codeanalyzer.schema.py_schema import PyDecorator
54
54
 
55
55
 
@@ -60,12 +60,17 @@ def project(app: PyApplication, app_name: str, sig_to_id: dict,
60
60
  passes it through so the :PyApplication node carries it as props."""
61
61
  b = RowBuilder()
62
62
 
63
+ # Keyed on the ``can://<app>`` id, not on ``--app-name``: two applications
64
+ # analyzed under the same free-text name used to MERGE onto one root, with
65
+ # no diagnostic. ``name`` survives as a display property.
63
66
  app_ref = b.node(
64
67
  ["PyApplication"],
65
- "name",
66
- app_name,
68
+ "id",
69
+ application_id(app_name),
67
70
  prune(
68
71
  {
72
+ "id": application_id(app_name),
73
+ "name": app_name,
69
74
  "schema_version": SCHEMA_VERSION,
70
75
  "analyzer_name": analyzer.name if analyzer else None,
71
76
  "analyzer_version": analyzer.version if analyzer else None,
@@ -252,17 +257,23 @@ def _project_program_graphs(
252
257
  # endpoint functions' identity maps), so they land on the very PyBodyNode ids
253
258
  # projected above — a formal_in global id equals _global_ordinal(callee.id,
254
259
  # "@formal_in:0"). No dangling references.
260
+ # `var` (#195): the callee-side formal's variable, set on every edge by the
261
+ # SDG assembler and carried on `ParamEdge.var`. The catalog declared it and
262
+ # nothing wrote it, so `r.var` predicates went three-valued across every
263
+ # call boundary.
255
264
  for e in app.param_in or []:
256
265
  b.edge(
257
266
  "PY_PARAM_IN",
258
267
  NodeRef("PyBodyNode", "id", e.src),
259
268
  NodeRef("PyBodyNode", "id", e.dst),
269
+ prune({"var": e.var}),
260
270
  )
261
271
  for e in app.param_out or []:
262
272
  b.edge(
263
273
  "PY_PARAM_OUT",
264
274
  NodeRef("PyBodyNode", "id", e.src),
265
275
  NodeRef("PyBodyNode", "id", e.dst),
276
+ prune({"var": e.var}),
266
277
  )
267
278
 
268
279
 
@@ -290,7 +301,7 @@ def _import_ghost(b: RowBuilder, app_can_id: str, name: str) -> NodeRef:
290
301
  projected too, both rows collapse onto this one node — correctly, since
291
302
  they name the same real-world symbol."""
292
303
  return b.node(
293
- ["PySymbol", "PyExternal"], "id", f"{app_can_id}/@external/{name}", {"name": name}
304
+ ["PySymbol", "PyExternal"], "id", external_id(app_can_id, None, name), {"name": name}
294
305
  )
295
306
 
296
307
 
@@ -503,7 +514,7 @@ def _external_ghost(b: RowBuilder, app_can_id: str, signature: str) -> NodeRef:
503
514
  ``_home_external_symbols`` uses — ``<app>/@external/<module>/<name>`` — so it
504
515
  sits inside the application prefix (#173) and MERGEs with a homed twin."""
505
516
  module, name = signature.rsplit(".", 1) if "." in signature else (None, signature)
506
- ext_id = f"{app_can_id}/@external/{module}/{name}" if module else f"{app_can_id}/@external/{name}"
517
+ ext_id = external_id(app_can_id, module, name)
507
518
  return b.node(["PySymbol", "PyExternal"], "id", ext_id, prune({"name": name, "module": module}))
508
519
 
509
520
 
@@ -28,6 +28,8 @@ from __future__ import annotations
28
28
  from dataclasses import dataclass, field
29
29
  from typing import Dict, List, Optional, Union
30
30
 
31
+ from codeanalyzer.schema.ids import SCHEME, application_id
32
+
31
33
  # A property value: a primitive, or a homogeneous list of primitives.
32
34
  Scalar = Union[str, int, float, bool]
33
35
  Prop = Union[Scalar, List[str], List[int], List[float], List[bool]]
@@ -57,28 +59,37 @@ class NodeRow:
57
59
  module: Optional[str] = None
58
60
 
59
61
 
60
- # The marker label on every node keyed by a ``can://python/`` id (#173). It is an
62
+ # The marker label on every node keyed by a ``can://`` id (#173). It is an
61
63
  # INDEX ANCHOR, nothing more: Neo4j property indexes are label-scoped, so the
62
64
  # prefix predicate ``id STARTS WITH $p`` needs a label to seek on. Safety comes
63
- # from the prefix, which carries language, application and module.
65
+ # from the prefix, which carries application, language and module.
66
+ #
67
+ # The test is the SCHEME, never a language segment: since the app moved outermost
68
+ # an id no longer begins with the language, and ``can://python/`` now means "the
69
+ # application is called python". Testing that here would have quietly stripped the
70
+ # marker off every graph but one, taking the destructive statements' index — and
71
+ # their reach — with it.
64
72
  CAN_NODE = "PyCanNode"
65
- _PY_CAN_PREFIX = "can://python/"
66
73
 
67
74
 
68
75
  def descendant_prefix(can_id: str) -> str:
69
76
  """The prefix that matches a node's descendants and nothing else. The separator
70
- is the point: ``can://python/app/src/foo.py`` is also a prefix of
71
- ``can://python/app/src/foo.pyX``, so descendants match on ``id + '/'`` and the
77
+ is the point: ``can://app/python/src/foo.py`` is also a prefix of
78
+ ``can://app/python/src/foo.pyX``, so descendants match on ``id + '/'`` and the
72
79
  node itself by equality."""
73
80
  return f"{can_id}/"
74
81
 
75
82
 
76
83
  def application_prefix(app_name: Optional[str]) -> str:
77
- """``can://python/<app>/`` — the scope of every destructive statement. Refuses an
78
- empty application: ``STARTS WITH ''`` would match every node in the database."""
84
+ """``can://<app>/`` — the scope of every destructive statement. Refuses an
85
+ empty application: ``STARTS WITH ''`` would match every node in the database.
86
+
87
+ Since the app is the outermost segment this now covers the application's
88
+ artifacts and config keys too, which the old language-first prefix left
89
+ outside every scope and so never cleaned up."""
79
90
  if not app_name:
80
91
  raise ValueError("neo4j: refusing a destructive statement without an application id")
81
- return descendant_prefix(f"{_PY_CAN_PREFIX}{app_name}")
92
+ return descendant_prefix(application_id(app_name))
82
93
 
83
94
 
84
95
  @dataclass
@@ -131,7 +142,7 @@ class RowBuilder:
131
142
  node_id = f"{labels[0]} {value}"
132
143
  props = dict(props)
133
144
  module = props.pop("_module", None) # lifted off the graph (#173)
134
- if key_prop == "id" and value.startswith(_PY_CAN_PREFIX) and CAN_NODE not in labels:
145
+ if key_prop == "id" and value.startswith(SCHEME) and CAN_NODE not in labels:
135
146
  labels = [*labels, CAN_NODE]
136
147
  existing = self._nodes.get(node_id)
137
148
  if existing is not None:
@@ -57,7 +57,7 @@ class RelType:
57
57
 
58
58
 
59
59
  # Labels layered onto a node in addition to its primary/specific label.
60
- # ``PyCanNode`` (#173) rides every node keyed by a ``can://python/`` id — an index
60
+ # ``PyCanNode`` (#173) rides every node keyed by a ``can://`` id — an index
61
61
  # anchor for the prefix-scoped destructive statements (see ``rows.CAN_NODE``).
62
62
  MARKER_LABELS: List[str] = ["PyCanNode"]
63
63
 
@@ -68,8 +68,9 @@ NODE_LABELS: List[NodeLabel] = [
68
68
  NodeLabel(
69
69
  "PyApplication",
70
70
  "PyApplication",
71
- "name",
71
+ "id",
72
72
  {
73
+ "id": "string",
73
74
  "name": "string",
74
75
  "schema_version": "string",
75
76
  "analyzer_name": "string",
@@ -1,17 +1,30 @@
1
1
  """Canonical `can://` id construction for schema v2 (durable ids, ≥ callable).
2
2
  Ordinal ids (< callable) are `ordinal_id(callable_id, tag)`. Pure functions;
3
- ids are opaque handles (the <file> segment itself contains '/')."""
3
+ ids are opaque handles (the <file> segment itself contains '/').
4
+
5
+ The **application is the outermost segment** and the language sits inside it:
6
+ ``can://<app>/python/<file>/<type>/<callable-sig>``. So ``can://<app>`` is a
7
+ prefix of every id this analyzer mints for that application — code, externals
8
+ and artifacts alike — which is what the prefix-scoped destructive statements
9
+ (#173) rely on. Nothing may be identified by its *language* prefix any more:
10
+ an application named ``python`` mints ``can://python/python/...``, so a test
11
+ for ``can://python/`` no longer means "a python id"; test the scheme instead."""
4
12
  from __future__ import annotations
5
- from typing import List
13
+ from typing import List, Optional
6
14
 
7
- _SCHEME = "can://python"
15
+ SCHEME = "can://"
16
+
17
+ # This analyzer's language segment, which sits INSIDE the app rather than above it.
18
+ LANG = "python"
8
19
 
9
20
  def application_id(app_name: str) -> str:
10
- return f"{_SCHEME}/{app_name}"
21
+ """``can://<app>`` — the application root, and the prefix every id below it shares."""
22
+ return f"{SCHEME}{app_name}"
11
23
 
12
24
  def module_id(app_name: str, file_key: str) -> str:
25
+ """``can://<app>/python/<relative-file-key>`` (separators normalized to ``/``)."""
13
26
  rel = file_key.replace("\\", "/").lstrip("./")
14
- return f"{application_id(app_name)}/{rel}"
27
+ return f"{application_id(app_name)}/{LANG}/{rel}"
15
28
 
16
29
  def child_id(parent_id: str, segment: str) -> str:
17
30
  return f"{parent_id}/{segment}"
@@ -23,6 +36,22 @@ def ordinal_id(callable_id: str, tag: str) -> str:
23
36
  return f"{callable_id}@{tag}"
24
37
 
25
38
 
39
+ def external_id(app_id: str, module: Optional[str], name: str) -> str:
40
+ """``can://<app>/@external/<module>/<name>`` — the home of a call-graph
41
+ endpoint that is not declared in the symbol table (an imported library or
42
+ builtin member). ``module`` is ``None`` for a dot-less signature, which drops
43
+ the segment.
44
+
45
+ Language-NEUTRAL, like ``artifact``: ``@external`` sits in the position the
46
+ language occupies for code nodes, so sibling analyzers over the same ``<app>``
47
+ name a library symbol identically and it is one node in a merged graph. The
48
+ cost is real and was accepted deliberately — two analyzers' notions of
49
+ ``os.getcwd`` are not necessarily the same thing, and merging them says they
50
+ are. TypeScript's form; java follows it."""
51
+ base = f"{app_id}/@external"
52
+ return f"{base}/{module}/{name}" if module else f"{base}/{name}"
53
+
54
+
26
55
  def global_ordinal(callable_id: str, local_key: str) -> str:
27
56
  """The GLOBAL ordinal id of a body node from its LOCAL key: synthetic keys
28
57
  (`@entry`, `@formal_in:0`) already carry the `@`; positional keys (`15:2`,
@@ -41,12 +70,13 @@ def stamp_body_ids(callable) -> None:
41
70
 
42
71
 
43
72
  def artifact_id(app_name: str, rel_path: str) -> str:
44
- """Language-neutral artifact id: ``can://artifact/<app>/<rel-path>``.
73
+ """Language-neutral artifact id: ``can://<app>/artifact/<rel-path>``.
45
74
 
46
- The first segment is a namespace (a language for code nodes, the literal
47
- ``artifact`` for files), so sibling analyzers over the same repo emit the
48
- same id for the same file."""
49
- return f"can://artifact/{app_name}/{rel_path}"
75
+ ``artifact`` is a reserved segment in the same position the language
76
+ occupies for code nodes, so sibling analyzers over the same repo (same
77
+ ``<app>``) still emit the same id for the same file — and, unlike the old
78
+ ``can://artifact/<app>/...``, it now sits inside the application prefix."""
79
+ return f"{application_id(app_name)}/artifact/{rel_path}"
50
80
 
51
81
 
52
82
  def config_key_id(artifact_id: str, dotted_key: str) -> str:
@@ -171,7 +171,12 @@ class SummaryEdge(BaseModel):
171
171
 
172
172
  @builder
173
173
  class ParamEdge(BaseModel):
174
+ """A `param_in` (actual_in → formal_in) or `param_out` (formal_out → actual_out)
175
+ edge at application scope. `var` is the callee-side formal's variable — the
176
+ parameter name, or `<return>` for the return port — always set (#195), the
177
+ same value codeanalyzer-typescript carries on its `param_in[].var`."""
174
178
  src: str; dst: str
179
+ var: Optional[str] = None
175
180
 
176
181
 
177
182
  @builder
@@ -481,7 +486,7 @@ class PyExternalSymbol(BaseModel):
481
486
  builtin member. An edge-endpoint id home, not a tree node: keyed in
482
487
  ``PyApplication.external_symbols`` by its ``can://…/@external/…`` id."""
483
488
 
484
- id: str = "" # can://python/<app>/@external/<module>/<name>
489
+ id: str = "" # can://<app>/@external/<module>/<name>
485
490
  kind: str = "external"
486
491
  name: str # the member/short name, e.g. "get" for "requests.get"
487
492
  module: Optional[str] = None # best-effort owning module, e.g. "requests"
@@ -510,7 +515,7 @@ class PyArtifact(BaseModel):
510
515
  plain data/binary) -- never dropped from the walk. Captured broadly (node
511
516
  + verbatim ``source``); *meaning* is extracted narrowly -- only
512
517
  ``dependency-manifest`` roles feed ``dependencies`` today. ``id`` is
513
- language-neutral (``can://artifact/<app>/<path>``)."""
518
+ language-neutral (``can://<app>/artifact/<path>``)."""
514
519
 
515
520
  id: str = ""
516
521
  kind: str = "artifact"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codeanalyzer-python
3
- Version: 1.4.1
3
+ Version: 1.5.1
4
4
  Summary: Static analysis for Python — canonical schema v2 (symbol table, call graph, and native CFG/PDG/SDG dataflow) as analysis.json or a Neo4j property graph.
5
5
  Author-email: Rahul Krishna <i.m.ralk@gmail.com>
6
6
  License-File: LICENSE
@@ -517,11 +517,11 @@ just populate more of the same tree:
517
517
  "k_limit": 3, // access-path depth bound (--graph-field-depth); L3+ only
518
518
  "analyzer": { "name": "codeanalyzer-python", "version": "1.0.0" },
519
519
  "application": {
520
- "id": "can://python/<app>",
520
+ "id": "can://<app>",
521
521
  "kind": "application",
522
522
  "symbol_table": { // relative POSIX path → module
523
523
  "pkg/mod.py": {
524
- "id": "can://python/<app>/pkg/mod.py",
524
+ "id": "can://<app>/python/pkg/mod.py",
525
525
  "kind": "module",
526
526
  "source": "…full file text, stored once per module…",
527
527
  "types": { "<Class>": { "id": "…", "kind": "class", "callables": { /* methods */ } } },
@@ -531,11 +531,12 @@ just populate more of the same tree:
531
531
  "call_graph": [ { "src": "can://…/main(a)", "dst": "can://…/helper(x)",
532
532
  "weight": 1, "prov": ["defuse", "jedi"] } ],
533
533
  "external_symbols": { // imported/builtin call targets, keyed by id
534
- "can://python/<app>/@external/os/getcwd":
535
- { "id": "can://python/<app>/@external/os/getcwd", "kind": "external",
534
+ "can://<app>/@external/os/getcwd":
535
+ { "id": "can://<app>/@external/os/getcwd", "kind": "external",
536
536
  "name": "getcwd", "module": "os" }
537
537
  },
538
- "param_in": [ { "src": "can://…/main(a)@6:4/actual_in:0", "dst": "can://…/helper(x)@formal_in:0" } ],
538
+ "param_in": [ { "src": "can://…/main(a)@6:4/actual_in:0", "dst": "can://…/helper(x)@formal_in:0",
539
+ "var": "x" } ], // var = the callee formal
539
540
  "param_out": [ { "src": "can://…/helper(x)@formal_out", "dst": "can://…/main(a)@6:4/actual_out" } ]
540
541
  }
541
542
  }
@@ -568,7 +569,7 @@ The application envelope also contains three substrate sections:
568
569
  - **`artifacts`** — discovered non-code files (manifests, configs, Docker files, CI workflows,
569
570
  packaging files, scripts, docs, and legal files) with extraction status (`none`, `partial`, or
570
571
  `full`; default `none`), keyed by relative path; each artifact carries the
571
- `can://artifact/<app>/<path>` id namespace. Config files carry extracted `config_keys`
572
+ `can://<app>/artifact/<path>` id namespace. Config files carry extracted `config_keys`
572
573
  (keys, values, namespaces, and references) and `DEFINES_CONFIG` Neo4j edges.
573
574
  - **`dependencies`** — declared packages with kind (`runtime`/`dev`/`optional`/`build`), spec,
574
575
  locked version, and provenance (`prov`): where each binding came from (manifest file, lock file,
@@ -579,7 +580,7 @@ The application envelope also contains three substrate sections:
579
580
  Notable properties:
580
581
 
581
582
  - **Durable `can://` ids** identify every node at callable granularity and above
582
- (`can://python/<app>/<file>/<callable-sig>`); nodes below a callable use ordinal ids
583
+ (`can://<app>/python/<file>/<callable-sig>`); nodes below a callable use ordinal ids
583
584
  (`@entry`, `@exit`, `line:col`, `@formal_in:N`, `line:col/actual_in:N`).
584
585
  - **`source` lives once per module**; every node's text is the `module.source[span.bytes]` slice.
585
586
  - **Cross-function edges** — `call_graph`, `param_in`, `param_out`, `config_uses` — live at **application** scope;
@@ -701,7 +702,7 @@ RETURN src.id, d.var, d.prov
701
702
 
702
703
  // interprocedural flow through a parameter (level 4)
703
704
  MATCH (a:PyBodyNode)-[:PY_PARAM_IN]->(f:PyBodyNode)
704
- WHERE f.id STARTS WITH "can://python/myapp/src/api.py"
705
+ WHERE f.id STARTS WITH "can://myapp/python/src/api.py"
705
706
  RETURN a.id, f.id
706
707
  ```
707
708
 
@@ -730,3 +731,25 @@ RETURN f.id, l.version
730
731
  ## License
731
732
 
732
733
  Apache 2.0 — see [LICENSE](./LICENSE).
734
+
735
+ ## Polyglot applications: all languages, or none
736
+
737
+ A `--emit neo4j` push is **destructive**. It sweeps everything under `can://<app>/` that this
738
+ analyzer marked, then rewrites what it found. Since the id grammar puts the application outermost,
739
+ every analyzer over the same `<app>` shares that prefix — so a push reclaims stale rows belonging to
740
+ *this* analyzer and, in the shared namespaces, sweeps rows a sibling wrote.
741
+
742
+ For most of what is shared that is harmless: the artifact walk is a whole-repo inventory, so an
743
+ `:Artifact` a sibling wrote is re-created by this push (with a thinner view of it — `roles` falls
744
+ back to `unknown` and its config keys and dependency edges are gone until that sibling pushes
745
+ again). `@external` ghosts are not inventoried that way: they are per-language, so a sibling's
746
+ ghosts are swept and not restored.
747
+
748
+ **So for an application analysed in more than one language, run every analyzer or none.** Running
749
+ one in isolation leaves the others' derived rows missing until they run again. Running them
750
+ together is always correct, in any order, because the last push restores everything the batch
751
+ swept.
752
+
753
+ Nothing here corrupts a graph: what is lost is derived and regenerates. But a partial run leaves a
754
+ partial answer, and nothing in the data says so.
755
+
@@ -1,6 +1,6 @@
1
1
  codeanalyzer/__init__.py,sha256=BZ3Kuwl-F_F-8H8cepLnVJ4Ku4NNUjjqg0Y6ujPQSsI,108
2
2
  codeanalyzer/__main__.py,sha256=o5_9ct61l3T7ryIsag8bz304NabVV-el_ooGD8tnbhA,15968
3
- codeanalyzer/core.py,sha256=tk_3dz81ECXXv8CEciWTIVfBAHHIfTVCsZte1GAZ080,46620
3
+ codeanalyzer/core.py,sha256=JfSsQEacBiVn38tQP34QWdJqQx7ecE70EXI8obISZN4,46598
4
4
  codeanalyzer/provenance.py,sha256=DT-DqwdVwO7g-GyK3sC0_4vDNCUjZYEY1fyYCt-7VRM,2306
5
5
  codeanalyzer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  codeanalyzer/artifacts/__init__.py,sha256=317sEeZLS1AYsfYDYIk7tLR3AdDFwWw_cmkeZDabkJo,906
@@ -13,7 +13,7 @@ codeanalyzer/artifacts/parsers.py,sha256=xOjC53tT0Mv0k7ONGwzl5XUvpBLnZlEcGmyrX5D
13
13
  codeanalyzer/dataflow/__init__.py,sha256=HCNOH24rAGLUMG94N9aC-2SsAWSntk3uys7e39ipU5o,1725
14
14
  codeanalyzer/dataflow/access_paths.py,sha256=wC8Q9qD-RZzkoFWMVvu_6uNNmYP8z48OGp9h9v3F1d4,23623
15
15
  codeanalyzer/dataflow/alias.py,sha256=ZYKY0DiR3GHv6YJr7C001Lode9rZsk6-gxdomUyIvwg,3928
16
- codeanalyzer/dataflow/builder.py,sha256=kUjI_oBHn5H8cfcSYxj40OHxiqXX-IbxXqahNLgffyQ,32812
16
+ codeanalyzer/dataflow/builder.py,sha256=grzgpl82KzNiwlgdTx60INMxkTZvddT3JzgaBRO4xh4,32839
17
17
  codeanalyzer/dataflow/cfg.py,sha256=u5YXJjAaDshdgv-9kWWITbBJFP-MwXKq6hbNh0fhDm8,25307
18
18
  codeanalyzer/dataflow/defuse.py,sha256=LrX1ToOZzR79NlAGg5T647UAFkpiEqEnT7t0K5RXOiI,4377
19
19
  codeanalyzer/dataflow/dominance.py,sha256=X7Ki1QRdVqaseYsJtiUL7m9MbcC2WHEv9b8bczSLFUA,5086
@@ -45,21 +45,21 @@ codeanalyzer/entrypoints/rules.yml,sha256=sguICRfDDNGjJPfZz-JSwkI_hzbTJaau1liHV_
45
45
  codeanalyzer/jedi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
46
  codeanalyzer/jedi/jedi.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
47
  codeanalyzer/neo4j/__init__.py,sha256=8Ei6uThTOqmul6GhnKoGOjLXJZbyDwF5Jo-8I8NxnAk,1574
48
- codeanalyzer/neo4j/bolt.py,sha256=kcBhujHuUst6s1eZD-vARDn0iuKUAxSHrN14nrsKGM8,13472
49
- codeanalyzer/neo4j/cypher.py,sha256=y2AW9OUAbZJ4TVHkFpHChJU3r_K7q81tTZKt0VNve6g,5677
48
+ codeanalyzer/neo4j/bolt.py,sha256=8l70iGDjY3Kdm-W1sb3b3O1uqLjDrVPYTpXhnkXdZOY,13694
49
+ codeanalyzer/neo4j/cypher.py,sha256=-wledvXVrcLJRtSC5MCn1N3tqgC06rNfPBn0w6POqHo,6239
50
50
  codeanalyzer/neo4j/emit.py,sha256=OBCO77TDTaNpqk9Di3JIQzBouLpHPdgoXuPSs44iJuU,3587
51
- codeanalyzer/neo4j/project.py,sha256=zoLsr2XGywlZbqnwlYvlhIpgu1jmIvSMTyAgIJZkmeM,38494
52
- codeanalyzer/neo4j/rows.py,sha256=PMlJEyLVIrKITkkK5TOutIzDgBuKLA1RGAkrQ1HLpZo,9579
53
- codeanalyzer/neo4j/schema.py,sha256=TyT3-Tu9thfWJTuvRtZ3BAIM4kQk13uUEJ-sgr9YAdE,14716
51
+ codeanalyzer/neo4j/project.py,sha256=wKvX7C1vamGswzoBRX09BZ1hOm1NJWoSdtQ7UNt2qTE,39102
52
+ codeanalyzer/neo4j/rows.py,sha256=n_J3NVIusLPsHMjPCWaYvlL5sFm6V-kl_Z6B_BTH8Fk,10140
53
+ codeanalyzer/neo4j/schema.py,sha256=ZRjBxpagBsQrPbD5BbeAZlj3ilTeYe4AsquztRDaH4M,14735
54
54
  codeanalyzer/options/__init__.py,sha256=6NewN0a-1HAEgKcxQjYyVn2WEDLrhax8h3WLgZddeqI,94
55
55
  codeanalyzer/options/options.py,sha256=2jDCcs74iSEOhz90LoRGts6PC8yV9IDPVxnLPNmonOA,1609
56
56
  codeanalyzer/schema/__init__.py,sha256=hIyz02abUJNPW8yUgguaeKflZjTmOZWbDiKICNBD7yk,4141
57
57
  codeanalyzer/schema/assign_ids.py,sha256=pQHluLaO5a6hFREp3vjVRsUeAfTYSJ95RXS9TZvnoys,1590
58
58
  codeanalyzer/schema/call_graph_ids.py,sha256=mWAhJDBsW8i-siJiINOHCXEm4qM6F_5se9-HuHWIFqU,568
59
- codeanalyzer/schema/ids.py,sha256=gBjpOlx4S_1JULlXoAHGommd5e81D5dwd5JSopYwdo8,2543
59
+ codeanalyzer/schema/ids.py,sha256=YuioNK3Y-M2NcCBiGH4AB4rDezlqu6ycNO0R3O-Jxho,4388
60
60
  codeanalyzer/schema/l1_body.py,sha256=_sca0mTkMRb-5lasep87faGjDxZJd9jGB0hmBx3uCgk,1757
61
61
  codeanalyzer/schema/l2_callees.py,sha256=CTcBeGoO04DstDC6h-1sHMbbIrHDEOMxP01U9VLBcfc,2327
62
- codeanalyzer/schema/py_schema.py,sha256=lbW7fIRWOdVLH0Ql0yYCYZzaS-iaACBLOuDPmXesjKs,24238
62
+ codeanalyzer/schema/py_schema.py,sha256=4xJto32nWqHx0dkge4qHsV2JZSjbyv4ojnqpyBm78sQ,24588
63
63
  codeanalyzer/semantic_analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
64
  codeanalyzer/semantic_analysis/call_graph.py,sha256=6YEB_wTn5-oQYLrbIhJYE0BsHl4fpWPoy5Hwd9mTnGc,11918
65
65
  codeanalyzer/semantic_analysis/defuse_linker.py,sha256=bSn1rCun28OQqSd2NOc9aVBSm47Gt9Iul4r_gwfqO1w,64930
@@ -70,9 +70,9 @@ codeanalyzer/syntactic_analysis/symbol_table_builder.py,sha256=eTknBDlUuuQd3JEwb
70
70
  codeanalyzer/utils/__init__.py,sha256=hC6VWdR5rerSqBxzu9KQHTASWqwrrYJv-CMDwrTlzkc,137
71
71
  codeanalyzer/utils/logging.py,sha256=Fw0tattPAOMs3o0JMjjXhRVLIF64f-SCcygUXF9jqeg,904
72
72
  codeanalyzer/utils/progress_bar.py,sha256=C9JtzVdd10lIxTv-KA6PebqjKWueC_vMGwVzAtHuHIw,2818
73
- codeanalyzer_python-1.4.1.dist-info/METADATA,sha256=tmpHTz20bMUPMGvpJp66Guu8NtDqo-SIFso_Sk0BoHI,43485
74
- codeanalyzer_python-1.4.1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
75
- codeanalyzer_python-1.4.1.dist-info/entry_points.txt,sha256=v4Vux0Nnx7sOntVk_CH7W9RX6SkIkvR1FQYq73oVlCQ,105
76
- codeanalyzer_python-1.4.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
77
- codeanalyzer_python-1.4.1.dist-info/licenses/NOTICE,sha256=MdVkNYqHJ20on2FmWvgD4WpMsX5mir33xdyPvTXd0A0,1223
78
- codeanalyzer_python-1.4.1.dist-info/RECORD,,
73
+ codeanalyzer_python-1.5.1.dist-info/METADATA,sha256=Ujo3QIsG8aBykzxm3Mjw_OLCD3b8oaiIrLP1wTvOVD8,44845
74
+ codeanalyzer_python-1.5.1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
75
+ codeanalyzer_python-1.5.1.dist-info/entry_points.txt,sha256=v4Vux0Nnx7sOntVk_CH7W9RX6SkIkvR1FQYq73oVlCQ,105
76
+ codeanalyzer_python-1.5.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
77
+ codeanalyzer_python-1.5.1.dist-info/licenses/NOTICE,sha256=MdVkNYqHJ20on2FmWvgD4WpMsX5mir33xdyPvTXd0A0,1223
78
+ codeanalyzer_python-1.5.1.dist-info/RECORD,,