codeanalyzer-python 1.3.0__py3-none-any.whl → 1.4.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/__main__.py CHANGED
@@ -175,7 +175,10 @@ def main(
175
175
  bool,
176
176
  typer.Option(
177
177
  "--eager/--lazy",
178
- help="Enable eager or lazy analysis. Defaults to lazy.",
178
+ help="Enable eager or lazy analysis. Defaults to lazy. Also gates every "
179
+ "destructive step of a '--emit neo4j' Bolt push: a lazy push only adds and "
180
+ "updates, an eager one also removes declarations and edges the source no "
181
+ "longer has.",
179
182
  ),
180
183
  ] = False,
181
184
  skip_tests: Annotated[
@@ -239,19 +242,11 @@ def main(
239
242
  typer.Option(
240
243
  "--artifact-text/--no-artifact-text",
241
244
  help="Capture verbatim `source` text on discovered artifacts. "
242
- "--no-artifact-text empties `source` everywhere (inventory unchanged).",
245
+ "`source` is the whole file; --no-artifact-text empties it "
246
+ "everywhere (inventory unchanged). sha256/size_bytes always "
247
+ "reflect the full file.",
243
248
  ),
244
249
  ] = True,
245
- artifact_text_max_bytes: Annotated[
246
- int,
247
- typer.Option(
248
- "--artifact-text-max-bytes",
249
- help="Per-file byte cap on captured artifact `source`; a decodable "
250
- "file over the cap is truncated (text_truncated=True). "
251
- "sha256/size_bytes always reflect the full file.",
252
- min=1,
253
- ),
254
- ] = 262144,
255
250
  ):
256
251
  # Determinism: pin the interpreter hash seed before any analysis (no-op
257
252
  # when PYTHONHASHSEED is already set; --version exits before this).
@@ -336,7 +331,6 @@ def main(
336
331
  verbosity=verbosity,
337
332
  entrypoint_rules=tuple(entrypoint_rules or ()),
338
333
  artifact_text=artifact_text,
339
- artifact_text_max_bytes=artifact_text_max_bytes,
340
334
  )
341
335
 
342
336
  _set_log_level(options.verbosity)
@@ -81,9 +81,9 @@ def _resolve_ref(manifest_path: str, ref: str) -> Optional[str]:
81
81
 
82
82
  def _full_text(project_dir: Path, path: str, art: PyArtifact) -> str:
83
83
  """Manifest/lock extraction must never depend on the stored ``source`` --
84
- that's capped by ``text_max_bytes`` and emptied by ``capture_text=False``
85
- (both payload-size controls on the JSON/Neo4j payload, not extraction
86
- controls). Read the real file fresh instead; fall back to ``art.source``
84
+ that's emptied by ``capture_text=False`` (a payload-size control on the
85
+ JSON/Neo4j payload, not an extraction control). Read the real file fresh
86
+ instead; fall back to ``art.source``
87
87
  only if it is gone (e.g. a synthetic artifact in a unit test, or the file
88
88
  vanished mid-run).
89
89
 
@@ -76,27 +76,11 @@ def _classify(rel_posix: str) -> Tuple[str, List[str]] | None:
76
76
  return None
77
77
 
78
78
 
79
- def _capture_source(
80
- raw: bytes, text: str, capture_text: bool, text_max_bytes: int
81
- ) -> Tuple[str, bool]:
82
- """Decide ``(source, text_truncated)`` for a decodable file.
83
-
84
- Slices ``raw`` (not ``text``) for the cap, so it is a true byte cap even
85
- when it lands inside a multi-byte character -- ``errors="ignore"`` drops
86
- the dangling partial char at the cut, so this never raises."""
87
- if not capture_text:
88
- return "", False
89
- if len(raw) <= text_max_bytes:
90
- return text, False
91
- return raw[:text_max_bytes].decode("utf-8", errors="ignore"), True
92
-
93
-
94
79
  def discover_artifacts(
95
80
  project_dir: Path,
96
81
  app_name: str,
97
82
  *,
98
83
  capture_text: bool = True,
99
- text_max_bytes: int = 262144,
100
84
  ) -> Dict[str, PyArtifact]:
101
85
  """Walk the project and return every file as an artifact, sorted by path.
102
86
 
@@ -109,15 +93,11 @@ def discover_artifacts(
109
93
  deliberate exception -- it IS rule-matched (a dependency-manifest), so it
110
94
  is captured like any other manifest despite the `.py` suffix.
111
95
 
112
- ``capture_text=False`` empties ``source`` everywhere (inventory otherwise
113
- identical); a decodable file over ``text_max_bytes`` gets a truncated
114
- ``source`` and ``text_truncated=True`` -- except a ``dependency-manifest``
115
- role artifact, which is always captured in full when decodable and
116
- ``capture_text`` is on: its source is what ``build_dependency_view``
117
- parses, not bulk/incidental content, so the byte cap does not apply to
118
- it (``capture_text=False`` still empties it like everything else).
119
- ``sha256``/``size_bytes`` always reflect the full file regardless of
120
- either knob."""
96
+ ``source`` is the WHOLE file or nothing -- never a prefix (#172). A
97
+ decodable file is captured in full; ``capture_text=False`` empties
98
+ ``source`` everywhere (inventory otherwise identical), and an undecodable
99
+ file gets ``""`` as ``binary``. ``sha256``/``size_bytes`` always reflect
100
+ the full file regardless."""
121
101
  out: Dict[str, PyArtifact] = {}
122
102
  for path in sorted(project_dir.rglob("*")):
123
103
  if not path.is_file():
@@ -148,20 +128,14 @@ def discover_artifacts(
148
128
  if decodable and "." not in name and text.startswith("#!"):
149
129
  roles = ["script"]
150
130
  if decodable:
151
- # A dependency-manifest's source IS the extracted meaning (build_
152
- # dependency_view parses it) -- the byte cap targets bulk/incidental
153
- # assets, never the files extraction depends on, so manifests are
154
- # exempt from it. capture_text=False still empties source (handled
155
- # inside _capture_source); only the byte CAP is bypassed here.
156
- cap = len(raw) if "dependency-manifest" in roles else text_max_bytes
157
- source, text_truncated = _capture_source(raw, text, capture_text, cap)
131
+ source = text if capture_text else ""
158
132
  else:
159
- fmt, source, text_truncated = "binary", "", False
133
+ fmt, source = "binary", ""
160
134
 
161
135
  out[rel_posix] = PyArtifact(
162
136
  id=artifact_id(app_name, rel_posix), path=rel_posix, format=fmt,
163
137
  roles=list(roles), size_bytes=len(raw),
164
138
  sha256=hashlib.sha256(raw).hexdigest(),
165
- source=source, text_truncated=text_truncated,
139
+ source=source,
166
140
  )
167
141
  return out
codeanalyzer/core.py CHANGED
@@ -40,9 +40,9 @@ from codeanalyzer.provenance import analyzer_info, repository_info
40
40
  def _artifact_full_text(project_dir: Path, path: str, art) -> str:
41
41
  """Mirrors ``artifacts.dependencies._full_text`` verbatim (not imported
42
42
  -- that name is module-private to ``dependencies.py``): config-key
43
- extraction (#152) must never depend on the stored ``source`` -- capped
44
- by ``text_max_bytes`` and emptied by ``capture_text=False`` (payload-size
45
- controls, not extraction controls). Read the real file fresh instead;
43
+ extraction (#152) must never depend on the stored ``source`` -- emptied
44
+ by ``capture_text=False`` (a payload-size control, not an extraction
45
+ control). Read the real file fresh instead;
46
46
  fall back to ``art.source`` only if it's gone (e.g. a synthetic artifact
47
47
  in a unit test, or the file vanished mid-run). Keep the two in sync if
48
48
  this logic changes."""
@@ -673,7 +673,6 @@ class Codeanalyzer:
673
673
  app.artifacts = discover_artifacts(
674
674
  self.project_dir, app_name,
675
675
  capture_text=self.options.artifact_text,
676
- text_max_bytes=self.options.artifact_text_max_bytes,
677
676
  )
678
677
  app.dependencies, app.unresolved_imports = build_dependency_view(
679
678
  app.artifacts,
@@ -442,8 +442,13 @@ def build_program_graphs(
442
442
  for t in cs.targets:
443
443
  call_edges.append((sig, t))
444
444
 
445
- summaries = compute_summaries(infos, sorted(set(call_edges)))
446
- return assemble_sdg(infos, summaries, k)
445
+ # The converged (facts, ddg) per function are threaded straight into the
446
+ # assembler rather than re-derived there (#155).
447
+ solutions: Dict[str, object] = {}
448
+ summaries = compute_summaries(
449
+ infos, sorted(set(call_edges)), solutions=solutions
450
+ )
451
+ return assemble_sdg(infos, summaries, k, solutions=solutions)
447
452
 
448
453
 
449
454
  def emit_l4(
@@ -386,8 +386,18 @@ def assemble_sdg(
386
386
  infos: Dict[str, FunctionInfo],
387
387
  summaries: Dict[str, FunctionSummary],
388
388
  k: int,
389
+ *,
390
+ solutions: Optional[Dict[str, Tuple[Dict[int, object], List[object]]]] = None,
389
391
  ) -> ProgramGraphsIR:
390
- """Stitch every function's PDG into the whole-program SDG."""
392
+ """Stitch every function's PDG into the whole-program SDG.
393
+
394
+ *solutions* optionally carries the converged ``(facts, ddg)`` that
395
+ :func:`~codeanalyzer.dataflow.summaries.compute_summaries` already
396
+ derived, sparing a second identical solve per function (#155). Omit it and
397
+ every function is re-solved, which is the historical behaviour and the
398
+ right posture whenever *summaries* did not come from an immediately
399
+ preceding run over these same *infos*.
400
+ """
391
401
  ir = ProgramGraphsIR(k_limit=k)
392
402
 
393
403
  # Pass 1: solve each function against the final summaries and lay out its
@@ -396,7 +406,12 @@ def assemble_sdg(
396
406
  formal_ids: Dict[str, Dict[str, int]] = {}
397
407
  for sig in sorted(infos):
398
408
  info = infos[sig]
399
- summary, facts, ddg = solve_function(info, summaries)
409
+ cached = solutions.get(sig) if solutions is not None else None
410
+ if cached is None:
411
+ summary, facts, ddg = solve_function(info, summaries)
412
+ else:
413
+ facts, ddg = cached
414
+ summary = summaries[sig]
400
415
  asm = _FunctionAssembler(info, summary, facts, ddg)
401
416
  asm.build_formals()
402
417
  assemblers[sig] = asm
@@ -199,19 +199,42 @@ def solve_function(
199
199
  def compute_summaries(
200
200
  infos: Dict[str, FunctionInfo],
201
201
  call_edges: List[Tuple[str, str]],
202
+ *,
203
+ solutions: Optional[Dict[str, Tuple[Dict[int, object], List[DDGEdge]]]] = None,
202
204
  ) -> Dict[str, FunctionSummary]:
203
205
  """Bottom-up composition over the SCC condensation DAG, monotone fixpoint
204
- within each SCC."""
206
+ within each SCC.
207
+
208
+ A **singleton SCC with no self-edge** is solved exactly once: the
209
+ condensation is processed bottom-up, so every callee summary it reads is
210
+ already final and a second pass could only recompute the same answer to
211
+ observe that nothing changed. Genuinely recursive SCCs (several members,
212
+ or one member calling itself) still iterate to fixpoint.
213
+
214
+ When *solutions* is supplied it receives each signature's converged
215
+ ``(facts, ddg)`` — the by-products of the final solve, which
216
+ :func:`~codeanalyzer.dataflow.sdg.assemble_sdg` would otherwise recompute
217
+ from scratch. They are the same values that a fresh solve against the
218
+ final summaries produces, because a converged pass is by definition one
219
+ in which no member's summary changed (#155).
220
+ """
205
221
  order = strongly_connected_components(sorted(infos), call_edges)
222
+ self_calls = {src for src, dst in call_edges if src == dst}
206
223
  summaries: Dict[str, FunctionSummary] = {}
207
224
  for scc in order:
208
225
  members = [s for s in scc if s in infos]
209
- changed = True
210
- while changed:
226
+ if not members:
227
+ continue
228
+ recursive = len(members) > 1 or members[0] in self_calls
229
+ while True:
211
230
  changed = False
212
231
  for sig in members:
213
- new, _, _ = solve_function(infos[sig], summaries)
232
+ new, facts, ddg = solve_function(infos[sig], summaries)
233
+ if solutions is not None:
234
+ solutions[sig] = (facts, ddg)
214
235
  if summaries.get(sig) != new:
215
236
  summaries[sig] = new
216
237
  changed = True
238
+ if not (recursive and changed):
239
+ break
217
240
  return summaries
@@ -27,11 +27,24 @@ Algorithm (the module subgraph is the unit of idempotent replacement):
27
27
  4. upsert edges owned by changed modules (+ the shared edges).
28
28
  5. on a FULL run only, prune modules whose source file vanished.
29
29
 
30
+ **A push never deletes by default** (#171). Steps 3 and 5 are the only destructive
31
+ ones and both run on ``eager`` (``--eager``) only; a default ``--lazy`` push is purely
32
+ additive — MERGE-upsert of nodes and edges, nothing removed. The cost of the default is
33
+ staleness: a declaration or a call edge the source no longer has stays in the graph until
34
+ an ``--eager`` push reconciles it. That is the deliberate trade — an incremental push into
35
+ a shared database should not be able to destroy anything, and the destructive rebuild is
36
+ opt-in under the same flag that already forces a clean analysis rebuild.
37
+
30
38
  Nodes are MERGE-upserted, never blindly deleted, so a declaration another
31
39
  (unchanged) module still references survives and its incoming edges stay valid.
32
40
  ``:PyExternal`` / ``:PyPackage`` / ``:PyDecorator`` are shared (no ``_module``) and are
33
41
  MERGE-only.
34
42
 
43
+ Every ``_module`` match is anchored on the python-owned labels
44
+ (``schema.MODULE_OWNED_PATTERN``). ``_module`` is a shared convention, not a python-private
45
+ one -- codeanalyzer-java and codeanalyzer-typescript set it on their nodes too -- so an
46
+ unlabelled match reaches a sibling analyzer's graph in a shared database (#171).
47
+
35
48
  The ``neo4j`` driver is imported lazily so it stays an optional dependency and
36
49
  off the default (json) output path entirely.
37
50
  """
@@ -41,7 +54,7 @@ from dataclasses import dataclass
41
54
  from typing import Dict, List, Optional
42
55
 
43
56
  from codeanalyzer.neo4j.rows import EdgeRow, GraphRows, NodeRow, chunk
44
- from codeanalyzer.neo4j.schema import CONSTRAINTS, INDEXES
57
+ from codeanalyzer.neo4j.schema import CONSTRAINTS, INDEXES, MODULE_OWNED_PATTERN
45
58
  from codeanalyzer.utils import logger
46
59
 
47
60
  DESCENDANTS = (
@@ -59,7 +72,7 @@ class BoltConfig:
59
72
  database: Optional[str] = None
60
73
 
61
74
 
62
- def bolt_writer(rows: GraphRows, cfg: BoltConfig, full_run: bool) -> None:
75
+ def bolt_writer(rows: GraphRows, cfg: BoltConfig, full_run: bool, eager: bool = False) -> None:
63
76
  try:
64
77
  import neo4j # noqa: WPS433 (lazy, optional dependency)
65
78
  except ImportError as exc: # pragma: no cover - exercised only without the extra
@@ -119,15 +132,26 @@ def bolt_writer(rows: GraphRows, cfg: BoltConfig, full_run: bool) -> None:
119
132
  _upsert_nodes(session, neo4j, shared)
120
133
 
121
134
  # 4. per changed module: purge owned edges + vanished decls, then upsert its nodes.
135
+ # The purge is the only destructive step in a push, so it runs on --eager only.
122
136
  for m in changed:
123
137
  nodes = by_module[m]
124
138
  keys = [n.value for n in nodes]
139
+ if not eager:
140
+ _upsert_nodes(session, neo4j, nodes)
141
+ continue
125
142
  with session() as s:
126
143
  def _purge(tx, module=m, node_keys=keys):
127
- tx.run("MATCH (x {_module: $m})-[r]->() DELETE r", m=module)
144
+ # Anchored on python-owned labels: `_module` is also set by the java
145
+ # and typescript analyzers, so an unlabelled match would delete a
146
+ # sibling's nodes wherever a file key collides (#171).
128
147
  tx.run(
129
- "MATCH (x {_module: $m}) "
130
- "WHERE NOT coalesce(x.signature, x.id, x.file_key) IN $keys "
148
+ f"MATCH (x:{MODULE_OWNED_PATTERN}) WHERE x._module = $m "
149
+ "MATCH (x)-[r]->() DELETE r",
150
+ m=module,
151
+ )
152
+ tx.run(
153
+ f"MATCH (x:{MODULE_OWNED_PATTERN}) WHERE x._module = $m "
154
+ "AND NOT coalesce(x.signature, x.id, x.file_key) IN $keys "
131
155
  "DETACH DELETE x",
132
156
  m=module,
133
157
  keys=node_keys,
@@ -147,7 +171,7 @@ def bolt_writer(rows: GraphRows, cfg: BoltConfig, full_run: bool) -> None:
147
171
  # 6. orphan prune — only safe on a full run (a targeted run can't tell deleted from untargeted).
148
172
  # Scope to THIS application's anchor so a full run for application B never
149
173
  # deletes application A's modules from a shared database.
150
- if full_run and app_name is not None:
174
+ if full_run and eager and app_name is not None:
151
175
  present = list(by_module.keys())
152
176
  with session() as s:
153
177
  res = s.run(
@@ -161,6 +185,11 @@ def bolt_writer(rows: GraphRows, cfg: BoltConfig, full_run: bool) -> None:
161
185
  pruned = res.single()
162
186
  pruned_count = pruned["pruned"] if pruned else 0
163
187
  logger.info(f"neo4j(bolt): pruned {pruned_count} vanished module(s)")
188
+ elif not eager:
189
+ logger.info(
190
+ "neo4j(bolt): additive push (--lazy) — nothing deleted; "
191
+ "re-run with --eager to reconcile removed declarations and edges"
192
+ )
164
193
  else:
165
194
  logger.info(
166
195
  "neo4j(bolt): targeted run — orphan pruning skipped (deleted files not removed)"
@@ -67,9 +67,10 @@ def emit_neo4j(analysis: Analysis, options: AnalysisOptions) -> None:
67
67
  password=options.neo4j_password,
68
68
  database=options.neo4j_database,
69
69
  )
70
- # A full run (no single-file restriction) makes orphan pruning safe.
70
+ # A full run (no single-file restriction) makes orphan pruning safe; --eager
71
+ # is what permits any deletion at all (#171).
71
72
  full_run = options.file_name is None
72
- bolt_writer(rows, cfg, full_run)
73
+ bolt_writer(rows, cfg, full_run, eager=options.rebuild_analysis)
73
74
  return
74
75
 
75
76
  out_dir = options.output if options.output is not None else Path.cwd()
@@ -309,7 +309,6 @@ def _project_artifacts(b: RowBuilder, app: PyApplication, app_name: str, app_ref
309
309
  "size_bytes": art.size_bytes,
310
310
  "sha256": art.sha256,
311
311
  "source": art.source,
312
- "text_truncated": art.text_truncated,
313
312
  "extraction": art.extraction,
314
313
  }
315
314
  ),
@@ -210,7 +210,7 @@ NODE_LABELS: List[NodeLabel] = [
210
210
  NodeLabel("Artifact", "Artifact", "id", {
211
211
  "id": "string", "path": "string", "format": "string",
212
212
  "roles": "string[]", "size_bytes": "integer", "sha256": "string",
213
- "source": "string", "text_truncated": "boolean", "extraction": "string",
213
+ "source": "string", "extraction": "string",
214
214
  }),
215
215
  NodeLabel("Package", "Package", "id", {
216
216
  "id": "string", "ecosystem": "string", "name": "string",
@@ -332,10 +332,27 @@ def uniqueness_constraints() -> list[str]:
332
332
 
333
333
  CONSTRAINTS: List[str] = uniqueness_constraints()
334
334
 
335
+ # The labels this analyzer owns per module -- the ones carrying the internal ``_module``
336
+ # provenance property. Derived from NODE_LABELS so a new module-scoped label is covered
337
+ # without a second list to maintain. `_module` is NOT python-private: codeanalyzer-java
338
+ # and codeanalyzer-typescript set the same property on their nodes, so every statement
339
+ # matching on it must be anchored to these labels or it matches a sibling analyzer's graph
340
+ # in a shared database (#171).
341
+ MODULE_OWNED_LABELS: List[str] = [n.label for n in NODE_LABELS if "_module" in n.properties]
342
+
343
+ # The label disjunction to anchor such a statement with: ``MATCH (x:PyModule|PyClass|...)``.
344
+ MODULE_OWNED_PATTERN: str = "|".join(MODULE_OWNED_LABELS)
345
+
335
346
  INDEXES: List[str] = [
336
347
  "CREATE INDEX py_callable_name IF NOT EXISTS FOR (c:PyCallable) ON (c.name)",
337
348
  "CREATE INDEX py_class_name IF NOT EXISTS FOR (c:PyClass) ON (c.name)",
338
349
  "CREATE FULLTEXT INDEX py_code_fts IF NOT EXISTS FOR (c:PyCallable) ON EACH [c.code, c.docstring]",
350
+ ] + [
351
+ # One per module-owned label: the incremental writer's per-module purge matches on
352
+ # `_module` once per changed module, which without these is a label scan per label per
353
+ # module -- quadratic on a full push (#171).
354
+ f"CREATE INDEX {label.lower()}_module IF NOT EXISTS FOR (x:{label}) ON (x._module)"
355
+ for label in MODULE_OWNED_LABELS
339
356
  ]
340
357
 
341
358
 
@@ -43,7 +43,6 @@ class AnalysisOptions:
43
43
  clear_cache: bool = False
44
44
  verbosity: int = 0
45
45
  entrypoint_rules: Tuple[Path, ...] = ()
46
- # Artifact text-capture controls (#157 follow-up): whether to capture
47
- # `source` at all, and the per-file byte cap before it truncates.
46
+ # Artifact text capture (#157 follow-up): whether to capture `source` at
47
+ # all. There is no byte cap -- `source` is the whole file or "" (#172).
48
48
  artifact_text: bool = True
49
- artifact_text_max_bytes: int = 262144
@@ -512,9 +512,8 @@ class PyArtifact(BaseModel):
512
512
  format: str # toml|yaml|json|ini|properties|requirements|dockerfile|text|binary
513
513
  roles: List[str] = []
514
514
  size_bytes: int = 0
515
- sha256: str = "" # always the full file's hash, even when source is truncated/empty
516
- source: str = "" # verbatim by default; "" for binary or when capture is disabled
517
- text_truncated: bool = False # True when `source` is a prefix, not the full file
515
+ sha256: str = "" # always the full file's hash, even when source is empty
516
+ source: str = "" # the WHOLE file, or "" for binary / when capture is disabled -- never a prefix
518
517
  extraction: str = "none" # none|partial|full
519
518
  config_keys: List[PyConfigKey] = [] # flattened config keys (#152); [] when not namespace-eligible
520
519
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codeanalyzer-python
3
- Version: 1.3.0
3
+ Version: 1.4.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
@@ -99,7 +99,9 @@ needs.
99
99
  **interprocedural SDG** (synthetic parameter vertices, `param_in`/`param_out`/`summary`,
100
100
  alias-aware DDG) at level 4 — all built in-process from the stdlib `ast`.
101
101
  - **Neo4j output** — project the analysis into a labeled property graph: a self-contained
102
- `graph.cypher` snapshot, or an **incremental** push to a live database over Bolt.
102
+ `graph.cypher` snapshot, or an **incremental** push to a live database over Bolt. A push is
103
+ **additive by default** and never deletes: `--eager` is what permits it to remove declarations
104
+ and edges the source no longer has.
103
105
  - **Versioned schema** — a machine-readable, version-stamped Neo4j schema contract (`--emit schema`),
104
106
  checked in as `schema.neo4j.json` (`2.0.0`) and shipped with every release.
105
107
  - **Incremental cache** — per-file results are cached under `.codeanalyzer`; `--lazy` (default)
@@ -300,7 +302,19 @@ $ canpy --help
300
302
  │ --eager --lazy Enable eager or │
301
303
  │ lazy analysis. │
302
304
  │ Defaults to │
303
- │ lazy.
305
+ │ lazy. Also gates
306
+ │ every │
307
+ │ destructive step │
308
+ │ of a '--emit │
309
+ │ neo4j' Bolt │
310
+ │ push: a lazy │
311
+ │ push only adds │
312
+ │ and updates, an │
313
+ │ eager one also │
314
+ │ removes │
315
+ │ declarations and │
316
+ │ edges the source │
317
+ │ no longer has. │
304
318
  │ [default: lazy] │
305
319
  │ --skip-tests --include-tests Skip test files │
306
320
  │ in analysis. │
@@ -361,26 +375,18 @@ $ canpy --help
361
375
  │ `source` text on │
362
376
  │ discovered │
363
377
  │ artifacts. │
378
+ │ `source` is the │
379
+ │ whole file; │
364
380
  │ --no-artifact-t… │
365
- │ empties `source`
381
+ │ empties it
366
382
  │ everywhere │
367
383
  │ (inventory │
368
384
  │ unchanged). │
369
- │ [default: │
370
- │ artifact-text] │
371
- │ --artifact-text-… <int range> Per-file byte │
372
- │ [x>=1] cap on captured │
373
- │ artifact │
374
- │ `source`; a │
375
- │ decodable file │
376
- │ over the cap is │
377
- │ truncated │
378
- │ (text_truncated… │
379
385
  │ sha256/size_byt… │
380
386
  │ always reflect │
381
387
  │ the full file. │
382
388
  │ [default: │
383
- 262144]
389
+ artifact-text]
384
390
  │ --help Show this │
385
391
  │ message and │
386
392
  │ exit. │
@@ -1,19 +1,19 @@
1
1
  codeanalyzer/__init__.py,sha256=BZ3Kuwl-F_F-8H8cepLnVJ4Ku4NNUjjqg0Y6ujPQSsI,108
2
- codeanalyzer/__main__.py,sha256=VVGmkwLGkcfCdbikp2FdXzpGZEA1md8n8WhNSxJWz1E,16080
3
- codeanalyzer/core.py,sha256=yJ14_jL2V4qDD6pEMEltfeOLcFYTMRkRnh0dyP6RVR4,46715
2
+ codeanalyzer/__main__.py,sha256=o5_9ct61l3T7ryIsag8bz304NabVV-el_ooGD8tnbhA,15968
3
+ codeanalyzer/core.py,sha256=tk_3dz81ECXXv8CEciWTIVfBAHHIfTVCsZte1GAZ080,46620
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
7
7
  codeanalyzer/artifacts/config_keys.py,sha256=ovwptAErLYutlCzUwvyim2kH6-6vUKxuRcBV2KJS71Y,26843
8
8
  codeanalyzer/artifacts/config_use.py,sha256=1-jGacW249dwF7b6DOrTFuJ7sA5Ho9khgrgZqND0nro,27173
9
9
  codeanalyzer/artifacts/config_use_rules.yml,sha256=fNgI6IceOsj78LYmgoDH-vQ4-Mfb2ZFGKIQhLuoL4ik,1483
10
- codeanalyzer/artifacts/dependencies.py,sha256=bURMUbBzHaF8bInRk-INp1GaGujsUSXnFqLtNyECUMI,10888
11
- codeanalyzer/artifacts/discovery.py,sha256=8as4RD5oNgZM2qyg19HUqsdq9eETwPDGQUOn4fZLOsQ,7427
10
+ codeanalyzer/artifacts/dependencies.py,sha256=h_-XV5KhrT-1Ytguw0c4fyI9zSFzn9Aj7kR66omvL3M,10853
11
+ codeanalyzer/artifacts/discovery.py,sha256=cUVRSdV82G2hVGhkRGV7u24-tEAxUBmH9nkTg24ur0Y,5968
12
12
  codeanalyzer/artifacts/parsers.py,sha256=xOjC53tT0Mv0k7ONGwzl5XUvpBLnZlEcGmyrX5DS45k,9035
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=OOj5J-O0WgBnLMSu6hhixkM8dVkUopwBR-wlQq0INhE,32463
16
+ codeanalyzer/dataflow/builder.py,sha256=KiISa2bIuBf_0N0v_PzlgMGm0sFtMICEwA-Y6DFXiM4,32687
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
@@ -21,9 +21,9 @@ codeanalyzer/dataflow/identity.py,sha256=WAIal6XchmQqdnXbvbEgu8J6vJdXNRdie1KHz1v
21
21
  codeanalyzer/dataflow/pdg.py,sha256=1Bm7AnoRqXBryZulII2idPw0feV8eZQ1VqqprQ8PW-g,3731
22
22
  codeanalyzer/dataflow/scalpel_oracle.py,sha256=FxRadKrTWar0VKHyQJM40n3cq25sld3Sj4lMdOm5NFk,11494
23
23
  codeanalyzer/dataflow/scc.py,sha256=Doa_0-5f3agCu_5UmeEDlCUErg34TtniKIv8Uqd7Jiw,3493
24
- codeanalyzer/dataflow/sdg.py,sha256=sTUUlYMB9uTKg9Yxiw-ScTBXogaQKcrSP6vksKXG14M,17841
24
+ codeanalyzer/dataflow/sdg.py,sha256=taDXkIg0BZUtDEJUjRJB_dxrFl6tKNOUdFyj0zCiso0,18533
25
25
  codeanalyzer/dataflow/slicing.py,sha256=lWZ7jHhlR8m7rECWJQXvwfs5GNZeJY55Cud3Ji5UbNU,3654
26
- codeanalyzer/dataflow/summaries.py,sha256=DOgesiymL6WgePrtkQBiS_Rd7SuCr3McD4Eq05Msb50,8023
26
+ codeanalyzer/dataflow/summaries.py,sha256=TLtc5h4bLBC4lViuMhMlEp5rp_nBpDPOl4_wfBuRUY4,9210
27
27
  codeanalyzer/dataflow/syntactic.py,sha256=AbHyXjKX_1xkGgKH48BpXYCWBauActGUwSMF_OD-uys,1124
28
28
  codeanalyzer/dataflow/scalpel/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
29
29
  codeanalyzer/dataflow/scalpel/README.md,sha256=YN-LxqYYDekIdhx39AG28sg9UySMmqvLQJivTvR2Fz8,1543
@@ -45,21 +45,21 @@ codeanalyzer/entrypoints/rules.yml,sha256=rgDglVOcUNXnQ5FXLMfnJbfI7xHRiRmegTP5bm
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=qEBtQlBjaMPpLQPUCOAR9jEqLxpXYJG5mtxdYAhjtMI,10134
48
+ codeanalyzer/neo4j/bolt.py,sha256=wobEBSQn5z9uCER3fNtTl98Q1z5WuXG-O1rPRoFwFWI,11949
49
49
  codeanalyzer/neo4j/cypher.py,sha256=a7YSmxxDbPcYc5gI4TW9sM6_WOE3RgLXTGBR9K8W8kw,5321
50
- codeanalyzer/neo4j/emit.py,sha256=QdrZWG3_IQHMcKCX4bFINpXvqZU2Qfsi9beIJovC2p4,3493
51
- codeanalyzer/neo4j/project.py,sha256=DcymijKIy9TvBUhzlB_0fDmoSA8WRufJU-1oVP1hH1w,35102
50
+ codeanalyzer/neo4j/emit.py,sha256=OBCO77TDTaNpqk9Di3JIQzBouLpHPdgoXuPSs44iJuU,3587
51
+ codeanalyzer/neo4j/project.py,sha256=U-2ZurR3aYFY0k8WGpEIYTE4YEET2pJeOyxqb9i_Xyo,35044
52
52
  codeanalyzer/neo4j/rows.py,sha256=med0XFa63PPUVUhJMq1TtVcJxgaVW39sgxBRZJDIJ_w,7811
53
- codeanalyzer/neo4j/schema.py,sha256=HXM-rG-NBuujXU2fXGAUb65sGRKKTXDGB7jr360qEYQ,14330
53
+ codeanalyzer/neo4j/schema.py,sha256=88F_biHRhd4NV1RSNvFCwLa062gKJb12_P_-vV6L-rE,15379
54
54
  codeanalyzer/options/__init__.py,sha256=6NewN0a-1HAEgKcxQjYyVn2WEDLrhax8h3WLgZddeqI,94
55
- codeanalyzer/options/options.py,sha256=3GF1m4AI-9NN2sWmhl8wrvi2HjMwofec-We3WqKs_HA,1642
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
59
  codeanalyzer/schema/ids.py,sha256=aOzsgVaOo6x72dnRgNxlnt6b5wbZzhTm0JOz6pdI57I,1672
60
60
  codeanalyzer/schema/l1_body.py,sha256=5Su347kwAPNflJDf7SvBR3sNXx9PVdGEml2pOpQCwo0,1684
61
61
  codeanalyzer/schema/l2_callees.py,sha256=CTcBeGoO04DstDC6h-1sHMbbIrHDEOMxP01U9VLBcfc,2327
62
- codeanalyzer/schema/py_schema.py,sha256=pN03WJcb96LegGSFs7HIBWzTE8bnn8T2adH9M9FJGgQ,23966
62
+ codeanalyzer/schema/py_schema.py,sha256=5G8K6uqBopU5iLwlUW30AE5A8jgTEdNaQmOeizhccag,23885
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.3.0.dist-info/METADATA,sha256=QAUQ1DK8997qsSI8-g4d294RfI7wLPljdOL4gi42Ofo,42305
74
- codeanalyzer_python-1.3.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
75
- codeanalyzer_python-1.3.0.dist-info/entry_points.txt,sha256=v4Vux0Nnx7sOntVk_CH7W9RX6SkIkvR1FQYq73oVlCQ,105
76
- codeanalyzer_python-1.3.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
77
- codeanalyzer_python-1.3.0.dist-info/licenses/NOTICE,sha256=MdVkNYqHJ20on2FmWvgD4WpMsX5mir33xdyPvTXd0A0,1223
78
- codeanalyzer_python-1.3.0.dist-info/RECORD,,
73
+ codeanalyzer_python-1.4.0.dist-info/METADATA,sha256=DJKj-64VCcSuUoxUbwsesDi7jSkBv69YSrsePcgAzGM,42786
74
+ codeanalyzer_python-1.4.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
75
+ codeanalyzer_python-1.4.0.dist-info/entry_points.txt,sha256=v4Vux0Nnx7sOntVk_CH7W9RX6SkIkvR1FQYq73oVlCQ,105
76
+ codeanalyzer_python-1.4.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
77
+ codeanalyzer_python-1.4.0.dist-info/licenses/NOTICE,sha256=MdVkNYqHJ20on2FmWvgD4WpMsX5mir33xdyPvTXd0A0,1223
78
+ codeanalyzer_python-1.4.0.dist-info/RECORD,,