codegraph-ir 0.2.0__tar.gz → 0.2.1__tar.gz
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.
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/PKG-INFO +1 -1
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/pyproject.toml +1 -1
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/__init__.py +1 -1
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/analyses/cfg.py +15 -1
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/languages/base.py +8 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/languages/go.py +4 -1
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/languages/python.py +29 -1
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/languages/typescript.py +13 -1
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_pins.py +24 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_slicer.py +66 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/.github/workflows/ci.yml +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/.github/workflows/release.yml +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/.gitignore +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/.pre-commit-config.yaml +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/.python-version +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/CLAUDE.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/Code-IR.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/LICENSE +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/README.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/RELEASING.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/action.yml +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/docs/README.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/docs/architecture-rules.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/docs/architecture.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/docs/development.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/docs/experiment-log.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/docs/feature-research.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/docs/gate-noise.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/docs/github-action.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/docs/languages.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/docs/plan-0.2-0.4.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/docs/roadmap.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/docs/status.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/docs/strategy.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/examples/agent-pr-contract-drift.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/schemas/component_spec.schema.json +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/analyses/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/analyses/call_graph.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/analyses/effects.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/analyses/entrypoints.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/analyses/param_flow.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/analyses/pdg.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/analyses/purity.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/analyses/reaching_defs.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/analyses/symbols.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/api/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/api/mcp_server.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/api/server.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/cli.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/config.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/export/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/export/graphml.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/export/html_viz.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/export/json_export.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/export/mermaid.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/export/neo4j.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/hooks.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/ir/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/ir/component_spec.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/ir/edges.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/ir/graph.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/ir/nodes.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/languages/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/languages/cache.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/languages/registry.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/manifest.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/pipeline.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/py.typed +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/regenerate/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/regenerate/prompt_pack.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/regenerate/regenerator.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/report/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/report/diff.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/report/flow.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/report/impact.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/report/lint.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/report/pack.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/report/pins.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/report/stats.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/slicing/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/slicing/slicer.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/sources/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/sources/base.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/sources/codeql_source.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/sources/joern_source.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/sources/tree_sitter_source.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/trace/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/trace/trace_map.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/verify.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/src/cgir/watch.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/conftest.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/fixtures/python_sample/orchestrator.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/fixtures/python_sample/pricing.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/fixtures/ts_sample/api/service.ts +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/fixtures/ts_sample/util.ts +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/integration/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/integration/test_api.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/integration/test_cli_scan.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/integration/test_hook.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/integration/test_init.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_action.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_call_graph.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_cfg.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_component_spec.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_diff.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_effects.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_entrypoints.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_go_adapter.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_graphml.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_html_viz.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_impact.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_impact_run.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_ir_graph.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_language_adapter.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_lint.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_manifest.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_mcp_tools.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_mermaid.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_pack.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_param_flow.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_pdg.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_purity.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_python_di.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_reaching_defs.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_regenerator.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_shapes.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_source_cache.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_stats.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_symbols.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_trace_map.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_tree_sitter_source.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_typescript_adapter.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_verify.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.2.1}/tests/unit/test_watch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codegraph-ir
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: The deterministic contract layer for AI-modified codebases — effects, purity, contracts, and blast radius, with zero LLM calls.
|
|
5
5
|
Project-URL: Homepage, https://github.com/asonkiya/llm-semantic-compilers
|
|
6
6
|
Project-URL: Repository, https://github.com/asonkiya/llm-semantic-compilers
|
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
# Distribution name (the import package and CLI command are both `cgir`;
|
|
7
7
|
# the PyPI project name `cgir` was already taken, so we ship as codegraph-ir).
|
|
8
8
|
name = "codegraph-ir"
|
|
9
|
-
version = "0.2.
|
|
9
|
+
version = "0.2.1"
|
|
10
10
|
description = "The deterministic contract layer for AI-modified codebases — effects, purity, contracts, and blast radius, with zero LLM calls."
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
license = { file = "LICENSE" }
|
|
@@ -79,7 +79,13 @@ def build(graph: RepoGraph, repo_path: Path, adapter: LanguageAdapter | None = N
|
|
|
79
79
|
body = file_adapter.function_body(func_ts)
|
|
80
80
|
if body is None:
|
|
81
81
|
continue
|
|
82
|
-
builder = _CFGBuilder(
|
|
82
|
+
builder = _CFGBuilder(
|
|
83
|
+
graph=graph,
|
|
84
|
+
owner=func,
|
|
85
|
+
source=source,
|
|
86
|
+
adapter=file_adapter,
|
|
87
|
+
global_names=file_adapter.global_declared_names(func_ts, source),
|
|
88
|
+
)
|
|
83
89
|
builder.build_block(body, predecessors=[func.id], controller=None)
|
|
84
90
|
|
|
85
91
|
|
|
@@ -89,6 +95,9 @@ class _CFGBuilder:
|
|
|
89
95
|
owner: Node
|
|
90
96
|
source: bytes
|
|
91
97
|
adapter: LanguageAdapter
|
|
98
|
+
# names declared global/nonlocal in this function: writes to them mutate
|
|
99
|
+
# outer state and are recorded as `mutates`, not local `writes`.
|
|
100
|
+
global_names: set[str] = field(default_factory=set)
|
|
92
101
|
_counter: int = field(default=0, init=False)
|
|
93
102
|
|
|
94
103
|
def build_block(
|
|
@@ -102,6 +111,11 @@ class _CFGBuilder:
|
|
|
102
111
|
|
|
103
112
|
def build_stmt(self, ts_node: TSNode, preds: list[str], controller: str | None) -> list[str]:
|
|
104
113
|
desc = self.adapter.describe_statement(ts_node, self.source)
|
|
114
|
+
if isinstance(desc, AssignDesc) and self.global_names:
|
|
115
|
+
outer = [w for w in desc.writes if w in self.global_names]
|
|
116
|
+
if outer:
|
|
117
|
+
desc.writes = [w for w in desc.writes if w not in self.global_names]
|
|
118
|
+
desc.mutates = list(desc.mutates) + outer
|
|
105
119
|
if isinstance(desc, BranchDesc):
|
|
106
120
|
return self._build_branch(ts_node, desc, preds, controller)
|
|
107
121
|
if isinstance(desc, LoopDesc):
|
|
@@ -264,6 +264,14 @@ class LanguageAdapter(ABC):
|
|
|
264
264
|
def describe_statement(self, node: TSNode, source: bytes) -> StatementDesc:
|
|
265
265
|
"""Classify one statement and extract its parts (see descriptors)."""
|
|
266
266
|
|
|
267
|
+
def global_declared_names(self, func_node: TSNode, source: bytes) -> set[str]:
|
|
268
|
+
"""Names this function declares as outer-scope (`global`/`nonlocal`).
|
|
269
|
+
|
|
270
|
+
Assignments to these names mutate state *outside* the function, so
|
|
271
|
+
the CFG builder records them as ``mutates`` rather than local
|
|
272
|
+
``writes``. Default: none (TS/Go have no such declaration form)."""
|
|
273
|
+
return set()
|
|
274
|
+
|
|
267
275
|
# --- phase 3: ingest extraction ------------------------------------------
|
|
268
276
|
|
|
269
277
|
@abstractmethod
|
|
@@ -295,8 +295,11 @@ class GoAdapter(LanguageAdapter):
|
|
|
295
295
|
first_decl = next(
|
|
296
296
|
(c for c in root.named_children if c.type not in {"comment", "package_clause"}), None
|
|
297
297
|
)
|
|
298
|
+
pinnable = {"function_declaration", "method_declaration", "type_declaration"}
|
|
298
299
|
module_pins = pin_index.module_pins(
|
|
299
|
-
first_decl.start_point[0]
|
|
300
|
+
first_decl.start_point[0]
|
|
301
|
+
if first_decl is not None and first_decl.type in pinnable
|
|
302
|
+
else None
|
|
300
303
|
)
|
|
301
304
|
if module_pins:
|
|
302
305
|
for decl in decls:
|
|
@@ -205,6 +205,28 @@ class PythonAdapter(LanguageAdapter):
|
|
|
205
205
|
def function_body(self, func_node: TSNode) -> TSNode | None:
|
|
206
206
|
return func_node.child_by_field_name("body")
|
|
207
207
|
|
|
208
|
+
def global_declared_names(self, func_node: TSNode, source: bytes) -> set[str]:
|
|
209
|
+
"""Names bound by `global`/`nonlocal` in *this* function's own body.
|
|
210
|
+
|
|
211
|
+
Nested function bodies are skipped — their declarations apply to
|
|
212
|
+
their own CFG walk, not this one."""
|
|
213
|
+
names: set[str] = set()
|
|
214
|
+
body = func_node.child_by_field_name("body")
|
|
215
|
+
if body is None:
|
|
216
|
+
return names
|
|
217
|
+
stack: list[TSNode] = [body]
|
|
218
|
+
while stack:
|
|
219
|
+
node = stack.pop()
|
|
220
|
+
if node.type in {"function_definition", "class_definition"}:
|
|
221
|
+
continue # a nested scope's declarations are not ours
|
|
222
|
+
if node.type in {"global_statement", "nonlocal_statement"}:
|
|
223
|
+
for child in node.named_children:
|
|
224
|
+
if child.type == "identifier":
|
|
225
|
+
names.add(_text(child, source))
|
|
226
|
+
continue
|
|
227
|
+
stack.extend(node.named_children)
|
|
228
|
+
return names
|
|
229
|
+
|
|
208
230
|
def block_statements(self, block: TSNode) -> list[TSNode]:
|
|
209
231
|
return [c for c in block.named_children if c.type != "comment"]
|
|
210
232
|
|
|
@@ -335,8 +357,14 @@ class PythonAdapter(LanguageAdapter):
|
|
|
335
357
|
for child in root.children:
|
|
336
358
|
decls.extend(self._top_level_decl(child, source, module_name, pin_index))
|
|
337
359
|
first_decl = next((c for c in root.children if c.type != "comment"), None)
|
|
360
|
+
# A header block only belongs to the first statement when that
|
|
361
|
+
# statement is *pinnable* (a definition) — touching an import keeps
|
|
362
|
+
# the pins module-level.
|
|
363
|
+
pinnable = {"function_definition", "class_definition", "decorated_definition"}
|
|
338
364
|
module_pins = pin_index.module_pins(
|
|
339
|
-
first_decl.start_point[0]
|
|
365
|
+
first_decl.start_point[0]
|
|
366
|
+
if first_decl is not None and first_decl.type in pinnable
|
|
367
|
+
else None
|
|
340
368
|
)
|
|
341
369
|
if module_pins:
|
|
342
370
|
for decl in decls:
|
|
@@ -391,8 +391,20 @@ class TypeScriptAdapter(LanguageAdapter):
|
|
|
391
391
|
for child in root.children:
|
|
392
392
|
decls.extend(self._top_level(child, source, rel_path, pin_index))
|
|
393
393
|
first_decl = next((c for c in root.children if c.type != "comment"), None)
|
|
394
|
+
# Only a pinnable definition claims an adjacent header block; an
|
|
395
|
+
# import keeps the pins module-level.
|
|
396
|
+
pinnable = {
|
|
397
|
+
"function_declaration",
|
|
398
|
+
"class_declaration",
|
|
399
|
+
"abstract_class_declaration",
|
|
400
|
+
"export_statement",
|
|
401
|
+
"lexical_declaration",
|
|
402
|
+
"variable_declaration",
|
|
403
|
+
}
|
|
394
404
|
module_pins = pin_index.module_pins(
|
|
395
|
-
first_decl.start_point[0]
|
|
405
|
+
first_decl.start_point[0]
|
|
406
|
+
if first_decl is not None and first_decl.type in pinnable
|
|
407
|
+
else None
|
|
396
408
|
)
|
|
397
409
|
if module_pins:
|
|
398
410
|
for decl in decls:
|
|
@@ -205,3 +205,27 @@ def test_pack_renders_pins() -> None:
|
|
|
205
205
|
specs = [_spec("m.f", pins=["pure", "no-net"])]
|
|
206
206
|
out = render_pack(build_pack(specs, "m.f"))
|
|
207
207
|
assert "Pinned: no-net, pure" in out or "Pinned: pure, no-net" in out
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_module_pin_adjacent_to_imports_still_applies(tmp_path: Path) -> None:
|
|
211
|
+
"""Real-world layout: the header pin sits directly above the imports.
|
|
212
|
+
|
|
213
|
+
Adjacency only disqualifies a header block when it touches a *pinnable
|
|
214
|
+
definition* — touching an import (or any plain statement) keeps it
|
|
215
|
+
module-level. Found dogfooding on camera-tracking.
|
|
216
|
+
"""
|
|
217
|
+
(tmp_path / "m.py").write_text(
|
|
218
|
+
"# cgir: no-net\nfrom typing import Iterable\n\n\ndef f(x):\n return x\n"
|
|
219
|
+
)
|
|
220
|
+
[spec] = _scan(tmp_path)
|
|
221
|
+
assert "no-net" in spec.pins
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_module_pin_adjacent_to_def_still_belongs_to_def(tmp_path: Path) -> None:
|
|
225
|
+
# No imports: header touching the first def is that def's pin, not module-wide.
|
|
226
|
+
(tmp_path / "m.py").write_text(
|
|
227
|
+
"# cgir: pure\ndef f(x):\n return x\n\n\ndef g(x):\n return x\n"
|
|
228
|
+
)
|
|
229
|
+
specs = {s.id: s for s in _scan(tmp_path)}
|
|
230
|
+
assert specs["m.f"].pins == ["pure"]
|
|
231
|
+
assert specs["m.g"].pins == []
|
|
@@ -264,3 +264,69 @@ def test_subscript_write_to_local_is_not_state_transformation(tmp_path: Path) ->
|
|
|
264
264
|
""",
|
|
265
265
|
)
|
|
266
266
|
assert specs["m.index"].kind == ComponentKind.pure_function
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_global_rebinding_is_state_transformer(tmp_path: Path) -> None:
|
|
270
|
+
"""`global x; x = ...` mutates module state — found dogfooding: a lazy-init
|
|
271
|
+
service accessor with 34 callers scanned as pure_function."""
|
|
272
|
+
specs = _specs_for(
|
|
273
|
+
tmp_path,
|
|
274
|
+
"""
|
|
275
|
+
_client = None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def get_client():
|
|
279
|
+
global _client
|
|
280
|
+
if _client is None:
|
|
281
|
+
_client = object()
|
|
282
|
+
return _client
|
|
283
|
+
""",
|
|
284
|
+
)
|
|
285
|
+
assert specs["m.get_client"].kind == ComponentKind.state_transformer
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_nonlocal_names_extracted_by_adapter(tmp_path: Path) -> None:
|
|
289
|
+
# nested functions aren't sliced as components (yet), so pin the adapter
|
|
290
|
+
# hook directly: nonlocal declarations are collected like global ones.
|
|
291
|
+
from cgir.languages.python import PythonAdapter
|
|
292
|
+
|
|
293
|
+
a = PythonAdapter()
|
|
294
|
+
src = b"def bump():\n nonlocal count\n count = count + 1\n"
|
|
295
|
+
fn = a.locate_function(a.parse(src), "bump", 0)
|
|
296
|
+
assert fn is not None
|
|
297
|
+
assert a.global_declared_names(fn, src) == {"count"}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def test_local_rebinding_of_same_name_stays_pure(tmp_path: Path) -> None:
|
|
301
|
+
# a plain local assignment (no global decl) is not a mutation
|
|
302
|
+
specs = _specs_for(
|
|
303
|
+
tmp_path,
|
|
304
|
+
"""
|
|
305
|
+
_client = None
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def pure_shadow():
|
|
309
|
+
_client = 5
|
|
310
|
+
return _client
|
|
311
|
+
""",
|
|
312
|
+
)
|
|
313
|
+
assert specs["m.pure_shadow"].kind == ComponentKind.pure_function
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def test_nested_global_decl_does_not_taint_outer(tmp_path: Path) -> None:
|
|
317
|
+
# the nested function's `global x` must not make the OUTER local write a mutation
|
|
318
|
+
specs = _specs_for(
|
|
319
|
+
tmp_path,
|
|
320
|
+
"""
|
|
321
|
+
x = 0
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def outer():
|
|
325
|
+
x = 1
|
|
326
|
+
def inner():
|
|
327
|
+
global x
|
|
328
|
+
x = 2
|
|
329
|
+
return x
|
|
330
|
+
""",
|
|
331
|
+
)
|
|
332
|
+
assert specs["m.outer"].kind == ComponentKind.pure_function
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|