codeanalyzer-python 1.4.1__py3-none-any.whl → 1.5.0__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 +2 -2
- codeanalyzer/neo4j/bolt.py +12 -6
- codeanalyzer/neo4j/cypher.py +15 -7
- codeanalyzer/neo4j/project.py +10 -5
- codeanalyzer/neo4j/rows.py +20 -9
- codeanalyzer/neo4j/schema.py +3 -2
- codeanalyzer/schema/ids.py +40 -10
- codeanalyzer/schema/py_schema.py +2 -2
- {codeanalyzer_python-1.4.1.dist-info → codeanalyzer_python-1.5.0.dist-info}/METADATA +30 -8
- {codeanalyzer_python-1.4.1.dist-info → codeanalyzer_python-1.5.0.dist-info}/RECORD +14 -14
- {codeanalyzer_python-1.4.1.dist-info → codeanalyzer_python-1.5.0.dist-info}/WHEEL +0 -0
- {codeanalyzer_python-1.4.1.dist-info → codeanalyzer_python-1.5.0.dist-info}/entry_points.txt +0 -0
- {codeanalyzer_python-1.4.1.dist-info → codeanalyzer_python-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {codeanalyzer_python-1.4.1.dist-info → codeanalyzer_python-1.5.0.dist-info}/licenses/NOTICE +0 -0
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 =
|
|
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
|
codeanalyzer/neo4j/bolt.py
CHANGED
|
@@ -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
|
|
45
|
-
<module-id> + '/'`` is containment, and it is 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
|
|
119
|
-
#
|
|
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
|
-
(
|
|
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
|
|
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:
|
codeanalyzer/neo4j/cypher.py
CHANGED
|
@@ -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
|
-
"""
|
|
72
|
-
Scoped by id prefix, so it is one
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
|
codeanalyzer/neo4j/project.py
CHANGED
|
@@ -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
|
-
"
|
|
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,
|
|
@@ -290,7 +295,7 @@ def _import_ghost(b: RowBuilder, app_can_id: str, name: str) -> NodeRef:
|
|
|
290
295
|
projected too, both rows collapse onto this one node — correctly, since
|
|
291
296
|
they name the same real-world symbol."""
|
|
292
297
|
return b.node(
|
|
293
|
-
["PySymbol", "PyExternal"], "id",
|
|
298
|
+
["PySymbol", "PyExternal"], "id", external_id(app_can_id, None, name), {"name": name}
|
|
294
299
|
)
|
|
295
300
|
|
|
296
301
|
|
|
@@ -503,7 +508,7 @@ def _external_ghost(b: RowBuilder, app_can_id: str, signature: str) -> NodeRef:
|
|
|
503
508
|
``_home_external_symbols`` uses — ``<app>/@external/<module>/<name>`` — so it
|
|
504
509
|
sits inside the application prefix (#173) and MERGEs with a homed twin."""
|
|
505
510
|
module, name = signature.rsplit(".", 1) if "." in signature else (None, signature)
|
|
506
|
-
ext_id =
|
|
511
|
+
ext_id = external_id(app_can_id, module, name)
|
|
507
512
|
return b.node(["PySymbol", "PyExternal"], "id", ext_id, prune({"name": name, "module": module}))
|
|
508
513
|
|
|
509
514
|
|
codeanalyzer/neo4j/rows.py
CHANGED
|
@@ -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
|
|
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
|
|
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/
|
|
71
|
-
``can://python/
|
|
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
|
|
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(
|
|
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(
|
|
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:
|
codeanalyzer/neo4j/schema.py
CHANGED
|
@@ -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
|
|
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
|
-
"
|
|
71
|
+
"id",
|
|
72
72
|
{
|
|
73
|
+
"id": "string",
|
|
73
74
|
"name": "string",
|
|
74
75
|
"schema_version": "string",
|
|
75
76
|
"analyzer_name": "string",
|
codeanalyzer/schema/ids.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
73
|
+
"""Language-neutral artifact id: ``can://<app>/artifact/<rel-path>``.
|
|
45
74
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
same id for the same file
|
|
49
|
-
|
|
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:
|
codeanalyzer/schema/py_schema.py
CHANGED
|
@@ -481,7 +481,7 @@ class PyExternalSymbol(BaseModel):
|
|
|
481
481
|
builtin member. An edge-endpoint id home, not a tree node: keyed in
|
|
482
482
|
``PyApplication.external_symbols`` by its ``can://…/@external/…`` id."""
|
|
483
483
|
|
|
484
|
-
id: str = "" # can
|
|
484
|
+
id: str = "" # can://<app>/@external/<module>/<name>
|
|
485
485
|
kind: str = "external"
|
|
486
486
|
name: str # the member/short name, e.g. "get" for "requests.get"
|
|
487
487
|
module: Optional[str] = None # best-effort owning module, e.g. "requests"
|
|
@@ -510,7 +510,7 @@ class PyArtifact(BaseModel):
|
|
|
510
510
|
plain data/binary) -- never dropped from the walk. Captured broadly (node
|
|
511
511
|
+ verbatim ``source``); *meaning* is extracted narrowly -- only
|
|
512
512
|
``dependency-manifest`` roles feed ``dependencies`` today. ``id`` is
|
|
513
|
-
language-neutral (``can
|
|
513
|
+
language-neutral (``can://<app>/artifact/<path>``)."""
|
|
514
514
|
|
|
515
515
|
id: str = ""
|
|
516
516
|
kind: str = "artifact"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: codeanalyzer-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
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
|
|
520
|
+
"id": "can://<app>",
|
|
521
521
|
"kind": "application",
|
|
522
522
|
"symbol_table": { // relative POSIX path → module
|
|
523
523
|
"pkg/mod.py": {
|
|
524
|
-
"id": "can
|
|
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,8 +531,8 @@ 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
|
|
535
|
-
{ "id": "can
|
|
534
|
+
"can://<app>/@external/os/getcwd":
|
|
535
|
+
{ "id": "can://<app>/@external/os/getcwd", "kind": "external",
|
|
536
536
|
"name": "getcwd", "module": "os" }
|
|
537
537
|
},
|
|
538
538
|
"param_in": [ { "src": "can://…/main(a)@6:4/actual_in:0", "dst": "can://…/helper(x)@formal_in:0" } ],
|
|
@@ -568,7 +568,7 @@ The application envelope also contains three substrate sections:
|
|
|
568
568
|
- **`artifacts`** — discovered non-code files (manifests, configs, Docker files, CI workflows,
|
|
569
569
|
packaging files, scripts, docs, and legal files) with extraction status (`none`, `partial`, or
|
|
570
570
|
`full`; default `none`), keyed by relative path; each artifact carries the
|
|
571
|
-
`can
|
|
571
|
+
`can://<app>/artifact/<path>` id namespace. Config files carry extracted `config_keys`
|
|
572
572
|
(keys, values, namespaces, and references) and `DEFINES_CONFIG` Neo4j edges.
|
|
573
573
|
- **`dependencies`** — declared packages with kind (`runtime`/`dev`/`optional`/`build`), spec,
|
|
574
574
|
locked version, and provenance (`prov`): where each binding came from (manifest file, lock file,
|
|
@@ -579,7 +579,7 @@ The application envelope also contains three substrate sections:
|
|
|
579
579
|
Notable properties:
|
|
580
580
|
|
|
581
581
|
- **Durable `can://` ids** identify every node at callable granularity and above
|
|
582
|
-
(`can
|
|
582
|
+
(`can://<app>/python/<file>/<callable-sig>`); nodes below a callable use ordinal ids
|
|
583
583
|
(`@entry`, `@exit`, `line:col`, `@formal_in:N`, `line:col/actual_in:N`).
|
|
584
584
|
- **`source` lives once per module**; every node's text is the `module.source[span.bytes]` slice.
|
|
585
585
|
- **Cross-function edges** — `call_graph`, `param_in`, `param_out`, `config_uses` — live at **application** scope;
|
|
@@ -701,7 +701,7 @@ RETURN src.id, d.var, d.prov
|
|
|
701
701
|
|
|
702
702
|
// interprocedural flow through a parameter (level 4)
|
|
703
703
|
MATCH (a:PyBodyNode)-[:PY_PARAM_IN]->(f:PyBodyNode)
|
|
704
|
-
WHERE f.id STARTS WITH "can://python/
|
|
704
|
+
WHERE f.id STARTS WITH "can://myapp/python/src/api.py"
|
|
705
705
|
RETURN a.id, f.id
|
|
706
706
|
```
|
|
707
707
|
|
|
@@ -730,3 +730,25 @@ RETURN f.id, l.version
|
|
|
730
730
|
## License
|
|
731
731
|
|
|
732
732
|
Apache 2.0 — see [LICENSE](./LICENSE).
|
|
733
|
+
|
|
734
|
+
## Polyglot applications: all languages, or none
|
|
735
|
+
|
|
736
|
+
A `--emit neo4j` push is **destructive**. It sweeps everything under `can://<app>/` that this
|
|
737
|
+
analyzer marked, then rewrites what it found. Since the id grammar puts the application outermost,
|
|
738
|
+
every analyzer over the same `<app>` shares that prefix — so a push reclaims stale rows belonging to
|
|
739
|
+
*this* analyzer and, in the shared namespaces, sweeps rows a sibling wrote.
|
|
740
|
+
|
|
741
|
+
For most of what is shared that is harmless: the artifact walk is a whole-repo inventory, so an
|
|
742
|
+
`:Artifact` a sibling wrote is re-created by this push (with a thinner view of it — `roles` falls
|
|
743
|
+
back to `unknown` and its config keys and dependency edges are gone until that sibling pushes
|
|
744
|
+
again). `@external` ghosts are not inventoried that way: they are per-language, so a sibling's
|
|
745
|
+
ghosts are swept and not restored.
|
|
746
|
+
|
|
747
|
+
**So for an application analysed in more than one language, run every analyzer or none.** Running
|
|
748
|
+
one in isolation leaves the others' derived rows missing until they run again. Running them
|
|
749
|
+
together is always correct, in any order, because the last push restores everything the batch
|
|
750
|
+
swept.
|
|
751
|
+
|
|
752
|
+
Nothing here corrupts a graph: what is lost is derived and regenerates. But a partial run leaves a
|
|
753
|
+
partial answer, and nothing in the data says so.
|
|
754
|
+
|
|
@@ -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=
|
|
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
|
|
@@ -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=
|
|
49
|
-
codeanalyzer/neo4j/cypher.py,sha256
|
|
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=
|
|
52
|
-
codeanalyzer/neo4j/rows.py,sha256=
|
|
53
|
-
codeanalyzer/neo4j/schema.py,sha256=
|
|
51
|
+
codeanalyzer/neo4j/project.py,sha256=gw4qmoIyOy6LabbpmBawvvb7yGEvgDV4fdmoV128eJA,38774
|
|
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=
|
|
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=
|
|
62
|
+
codeanalyzer/schema/py_schema.py,sha256=Ujre6PoBQZ3DUn9tjoPsqoYBPtAcYW-WI2JphLaMKyU,24231
|
|
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.
|
|
74
|
-
codeanalyzer_python-1.
|
|
75
|
-
codeanalyzer_python-1.
|
|
76
|
-
codeanalyzer_python-1.
|
|
77
|
-
codeanalyzer_python-1.
|
|
78
|
-
codeanalyzer_python-1.
|
|
73
|
+
codeanalyzer_python-1.5.0.dist-info/METADATA,sha256=X4LXZPtqeXauN-M0hAsMBJCjJxqgOTeCj_KtYOBTgtI,44750
|
|
74
|
+
codeanalyzer_python-1.5.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
75
|
+
codeanalyzer_python-1.5.0.dist-info/entry_points.txt,sha256=v4Vux0Nnx7sOntVk_CH7W9RX6SkIkvR1FQYq73oVlCQ,105
|
|
76
|
+
codeanalyzer_python-1.5.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
77
|
+
codeanalyzer_python-1.5.0.dist-info/licenses/NOTICE,sha256=MdVkNYqHJ20on2FmWvgD4WpMsX5mir33xdyPvTXd0A0,1223
|
|
78
|
+
codeanalyzer_python-1.5.0.dist-info/RECORD,,
|
|
File without changes
|
{codeanalyzer_python-1.4.1.dist-info → codeanalyzer_python-1.5.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{codeanalyzer_python-1.4.1.dist-info → codeanalyzer_python-1.5.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|