graphitect 0.2.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.
- graphify/__init__.py +30 -0
- graphify/__main__.py +757 -0
- graphify/_minhash.py +107 -0
- graphify/affected.py +318 -0
- graphify/always_on/agents-md.md +12 -0
- graphify/always_on/antigravity-rules.md +14 -0
- graphify/always_on/claude-md.md +9 -0
- graphify/always_on/gemini-md.md +9 -0
- graphify/always_on/kiro-steering.md +5 -0
- graphify/always_on/vscode-instructions.md +17 -0
- graphify/analyze.py +769 -0
- graphify/benchmark.py +152 -0
- graphify/build.py +2300 -0
- graphify/cache.py +1746 -0
- graphify/callflow_html.py +2051 -0
- graphify/cargo_introspect.py +109 -0
- graphify/cli.py +4745 -0
- graphify/cluster.py +409 -0
- graphify/command-kilo.md +15 -0
- graphify/cross_repo_calls.py +216 -0
- graphify/cross_repo_types.py +75 -0
- graphify/csharp_dispatch.py +154 -0
- graphify/dedup.py +1213 -0
- graphify/detect.py +2566 -0
- graphify/diagnostics.py +406 -0
- graphify/export.py +1349 -0
- graphify/exporters/__init__.py +1 -0
- graphify/exporters/base.py +14 -0
- graphify/exporters/graphdb.py +173 -0
- graphify/exporters/html.py +637 -0
- graphify/extract.py +7856 -0
- graphify/extractors/MIGRATION.md +107 -0
- graphify/extractors/__init__.py +66 -0
- graphify/extractors/apex.py +215 -0
- graphify/extractors/base.py +85 -0
- graphify/extractors/bash.py +579 -0
- graphify/extractors/blade.py +53 -0
- graphify/extractors/commonlisp.py +540 -0
- graphify/extractors/csharp.py +448 -0
- graphify/extractors/dart.py +564 -0
- graphify/extractors/dm.py +494 -0
- graphify/extractors/elixir.py +241 -0
- graphify/extractors/engine.py +6509 -0
- graphify/extractors/fortran.py +311 -0
- graphify/extractors/go.py +527 -0
- graphify/extractors/json_config.py +240 -0
- graphify/extractors/julia.py +289 -0
- graphify/extractors/markdown.py +408 -0
- graphify/extractors/models.py +131 -0
- graphify/extractors/objc.py +566 -0
- graphify/extractors/ocaml.py +289 -0
- graphify/extractors/pascal.py +688 -0
- graphify/extractors/pascal_forms.py +196 -0
- graphify/extractors/powershell.py +522 -0
- graphify/extractors/razor.py +192 -0
- graphify/extractors/resolution.py +3584 -0
- graphify/extractors/robot.py +296 -0
- graphify/extractors/rust.py +470 -0
- graphify/extractors/sln.py +92 -0
- graphify/extractors/sql.py +720 -0
- graphify/extractors/terraform.py +181 -0
- graphify/extractors/verilog.py +329 -0
- graphify/extractors/zig.py +181 -0
- graphify/file_slice.py +246 -0
- graphify/global_graph.py +194 -0
- graphify/google_workspace.py +237 -0
- graphify/hooks.py +933 -0
- graphify/ids.py +93 -0
- graphify/ingest.py +358 -0
- graphify/install.py +2366 -0
- graphify/llm.py +3544 -0
- graphify/manifest.py +4 -0
- graphify/manifest_ingest.py +311 -0
- graphify/mcp_ingest.py +386 -0
- graphify/multigraph_compat.py +212 -0
- graphify/pascal_resolution.py +129 -0
- graphify/paths.py +436 -0
- graphify/pg_introspect.py +165 -0
- graphify/prs.py +770 -0
- graphify/querylog.py +80 -0
- graphify/reflect.py +882 -0
- graphify/report.py +346 -0
- graphify/resolver_registry.py +85 -0
- graphify/ruby_resolution.py +242 -0
- graphify/scip_ingest.py +363 -0
- graphify/security.py +460 -0
- graphify/semantic_cleanup.py +336 -0
- graphify/serve.py +2608 -0
- graphify/skill-agents.md +710 -0
- graphify/skill-aider.md +1283 -0
- graphify/skill-amp.md +710 -0
- graphify/skill-claw.md +713 -0
- graphify/skill-codex.md +710 -0
- graphify/skill-copilot.md +713 -0
- graphify/skill-devin.md +1410 -0
- graphify/skill-droid.md +710 -0
- graphify/skill-kilo.md +722 -0
- graphify/skill-kiro.md +713 -0
- graphify/skill-opencode.md +705 -0
- graphify/skill-pi.md +713 -0
- graphify/skill-trae.md +711 -0
- graphify/skill-vscode.md +709 -0
- graphify/skill-windows.md +755 -0
- graphify/skill.md +713 -0
- graphify/skills/agents/references/add-watch.md +56 -0
- graphify/skills/agents/references/exports.md +87 -0
- graphify/skills/agents/references/extraction-spec.md +70 -0
- graphify/skills/agents/references/github-and-merge.md +46 -0
- graphify/skills/agents/references/hooks.md +33 -0
- graphify/skills/agents/references/query.md +311 -0
- graphify/skills/agents/references/transcribe.md +52 -0
- graphify/skills/agents/references/update.md +210 -0
- graphify/skills/amp/references/add-watch.md +56 -0
- graphify/skills/amp/references/exports.md +87 -0
- graphify/skills/amp/references/extraction-spec.md +70 -0
- graphify/skills/amp/references/github-and-merge.md +46 -0
- graphify/skills/amp/references/hooks.md +33 -0
- graphify/skills/amp/references/query.md +311 -0
- graphify/skills/amp/references/transcribe.md +52 -0
- graphify/skills/amp/references/update.md +210 -0
- graphify/skills/claude/references/add-watch.md +56 -0
- graphify/skills/claude/references/exports.md +87 -0
- graphify/skills/claude/references/extraction-spec.md +70 -0
- graphify/skills/claude/references/github-and-merge.md +46 -0
- graphify/skills/claude/references/hooks.md +33 -0
- graphify/skills/claude/references/query.md +311 -0
- graphify/skills/claude/references/transcribe.md +52 -0
- graphify/skills/claude/references/update.md +210 -0
- graphify/skills/claw/references/add-watch.md +56 -0
- graphify/skills/claw/references/exports.md +87 -0
- graphify/skills/claw/references/extraction-spec.md +31 -0
- graphify/skills/claw/references/github-and-merge.md +46 -0
- graphify/skills/claw/references/hooks.md +33 -0
- graphify/skills/claw/references/query.md +311 -0
- graphify/skills/claw/references/transcribe.md +52 -0
- graphify/skills/claw/references/update.md +210 -0
- graphify/skills/codex/references/add-watch.md +56 -0
- graphify/skills/codex/references/exports.md +87 -0
- graphify/skills/codex/references/extraction-spec.md +31 -0
- graphify/skills/codex/references/github-and-merge.md +46 -0
- graphify/skills/codex/references/hooks.md +33 -0
- graphify/skills/codex/references/query.md +311 -0
- graphify/skills/codex/references/transcribe.md +52 -0
- graphify/skills/codex/references/update.md +210 -0
- graphify/skills/copilot/references/add-watch.md +56 -0
- graphify/skills/copilot/references/exports.md +87 -0
- graphify/skills/copilot/references/extraction-spec.md +70 -0
- graphify/skills/copilot/references/github-and-merge.md +46 -0
- graphify/skills/copilot/references/hooks.md +33 -0
- graphify/skills/copilot/references/query.md +311 -0
- graphify/skills/copilot/references/transcribe.md +52 -0
- graphify/skills/copilot/references/update.md +210 -0
- graphify/skills/droid/references/add-watch.md +56 -0
- graphify/skills/droid/references/exports.md +87 -0
- graphify/skills/droid/references/extraction-spec.md +70 -0
- graphify/skills/droid/references/github-and-merge.md +46 -0
- graphify/skills/droid/references/hooks.md +33 -0
- graphify/skills/droid/references/query.md +311 -0
- graphify/skills/droid/references/transcribe.md +52 -0
- graphify/skills/droid/references/update.md +210 -0
- graphify/skills/kilo/references/add-watch.md +56 -0
- graphify/skills/kilo/references/exports.md +87 -0
- graphify/skills/kilo/references/extraction-spec.md +70 -0
- graphify/skills/kilo/references/github-and-merge.md +46 -0
- graphify/skills/kilo/references/hooks.md +33 -0
- graphify/skills/kilo/references/query.md +311 -0
- graphify/skills/kilo/references/transcribe.md +52 -0
- graphify/skills/kilo/references/update.md +210 -0
- graphify/skills/kiro/references/add-watch.md +56 -0
- graphify/skills/kiro/references/exports.md +87 -0
- graphify/skills/kiro/references/extraction-spec.md +31 -0
- graphify/skills/kiro/references/github-and-merge.md +46 -0
- graphify/skills/kiro/references/hooks.md +33 -0
- graphify/skills/kiro/references/query.md +311 -0
- graphify/skills/kiro/references/transcribe.md +52 -0
- graphify/skills/kiro/references/update.md +210 -0
- graphify/skills/opencode/references/add-watch.md +56 -0
- graphify/skills/opencode/references/exports.md +87 -0
- graphify/skills/opencode/references/extraction-spec.md +70 -0
- graphify/skills/opencode/references/github-and-merge.md +46 -0
- graphify/skills/opencode/references/hooks.md +33 -0
- graphify/skills/opencode/references/query.md +311 -0
- graphify/skills/opencode/references/transcribe.md +52 -0
- graphify/skills/opencode/references/update.md +210 -0
- graphify/skills/pi/references/add-watch.md +56 -0
- graphify/skills/pi/references/exports.md +87 -0
- graphify/skills/pi/references/extraction-spec.md +31 -0
- graphify/skills/pi/references/github-and-merge.md +46 -0
- graphify/skills/pi/references/hooks.md +33 -0
- graphify/skills/pi/references/query.md +311 -0
- graphify/skills/pi/references/transcribe.md +52 -0
- graphify/skills/pi/references/update.md +210 -0
- graphify/skills/trae/references/add-watch.md +56 -0
- graphify/skills/trae/references/exports.md +87 -0
- graphify/skills/trae/references/extraction-spec.md +70 -0
- graphify/skills/trae/references/github-and-merge.md +46 -0
- graphify/skills/trae/references/hooks.md +35 -0
- graphify/skills/trae/references/query.md +311 -0
- graphify/skills/trae/references/transcribe.md +52 -0
- graphify/skills/trae/references/update.md +210 -0
- graphify/skills/vscode/references/add-watch.md +56 -0
- graphify/skills/vscode/references/exports.md +87 -0
- graphify/skills/vscode/references/extraction-spec.md +70 -0
- graphify/skills/vscode/references/github-and-merge.md +46 -0
- graphify/skills/vscode/references/hooks.md +33 -0
- graphify/skills/vscode/references/query.md +311 -0
- graphify/skills/vscode/references/transcribe.md +52 -0
- graphify/skills/vscode/references/update.md +210 -0
- graphify/skills/windows/references/add-watch.md +56 -0
- graphify/skills/windows/references/exports.md +87 -0
- graphify/skills/windows/references/extraction-spec.md +70 -0
- graphify/skills/windows/references/github-and-merge.md +46 -0
- graphify/skills/windows/references/hooks.md +33 -0
- graphify/skills/windows/references/query.md +311 -0
- graphify/skills/windows/references/transcribe.md +52 -0
- graphify/skills/windows/references/update.md +210 -0
- graphify/symbol_resolution.py +556 -0
- graphify/transcribe.py +186 -0
- graphify/tree_html.py +603 -0
- graphify/validate.py +95 -0
- graphify/watch.py +2280 -0
- graphify/wiki.py +405 -0
- graphitect/__init__.py +28 -0
- graphitect/__main__.py +4 -0
- graphitect/_vendor/__init__.py +2 -0
- graphitect/_vendor/archify/LICENSE +22 -0
- graphitect/_vendor/archify/SKILL.md +137 -0
- graphitect/_vendor/archify/THIRD_PARTY_NOTICES.md +69 -0
- graphitect/_vendor/archify/assets/JetBrainsMono-OFL.txt +93 -0
- graphitect/_vendor/archify/assets/template.html +14935 -0
- graphitect/_vendor/archify/bin/archify.mjs +2091 -0
- graphitect/_vendor/archify/bin/open-artifact.mjs +86 -0
- graphitect/_vendor/archify/bin/preview.mjs +653 -0
- graphitect/_vendor/archify/bin/visual-check.mjs +829 -0
- graphitect/_vendor/archify/brand-marks/README.md +31 -0
- graphitect/_vendor/archify/brand-marks/catalog.json +131 -0
- graphitect/_vendor/archify/delta/architecture-delta.mjs +1221 -0
- graphitect/_vendor/archify/examples/agent-run.lifecycle.json +60 -0
- graphitect/_vendor/archify/examples/agent-tool-call.workflow.json +94 -0
- graphitect/_vendor/archify/examples/async-job-roundtrip.sequence.json +61 -0
- graphitect/_vendor/archify/examples/brand-aware-delivery.architecture.json +47 -0
- graphitect/_vendor/archify/examples/cache-miss-request.sequence.json +82 -0
- graphitect/_vendor/archify/examples/checkout-platform.base.architecture.json +31 -0
- graphitect/_vendor/archify/examples/checkout-platform.head.architecture.json +31 -0
- graphitect/_vendor/archify/examples/dataflow-product-analytics.html +15045 -0
- graphitect/_vendor/archify/examples/deployment-release.lifecycle.json +49 -0
- graphitect/_vendor/archify/examples/event-stream.dataflow.json +57 -0
- graphitect/_vendor/archify/examples/incident-response.workflow.json +64 -0
- graphitect/_vendor/archify/examples/lifecycle-agent-run.html +14980 -0
- graphitect/_vendor/archify/examples/product-analytics.dataflow.json +76 -0
- graphitect/_vendor/archify/examples/production-deployment.architecture.json +71 -0
- graphitect/_vendor/archify/examples/release-delivery.workflow.json +62 -0
- graphitect/_vendor/archify/examples/sequence-cache-miss-request.html +15060 -0
- graphitect/_vendor/archify/examples/web-app-rendered.html +15009 -0
- graphitect/_vendor/archify/examples/web-app.architecture.json +46 -0
- graphitect/_vendor/archify/examples/workflow-agent-tool-call-rendered.html +15051 -0
- graphitect/_vendor/archify/migrations/workflow-v2.mjs +279 -0
- graphitect/_vendor/archify/package-lock.json +149 -0
- graphitect/_vendor/archify/package.json +39 -0
- graphitect/_vendor/archify/recipes/scenarios.mjs +391 -0
- graphitect/_vendor/archify/references/authoring-contract.md +243 -0
- graphitect/_vendor/archify/references/brand-marks.md +65 -0
- graphitect/_vendor/archify/references/delivery-contract.md +120 -0
- graphitect/_vendor/archify/references/viewer-runtime.md +45 -0
- graphitect/_vendor/archify/renderers/architecture/grid.mjs +62 -0
- graphitect/_vendor/archify/renderers/architecture/render-architecture.mjs +1078 -0
- graphitect/_vendor/archify/renderers/dataflow/README.md +104 -0
- graphitect/_vendor/archify/renderers/dataflow/render-dataflow.mjs +483 -0
- graphitect/_vendor/archify/renderers/lifecycle/README.md +115 -0
- graphitect/_vendor/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
- graphitect/_vendor/archify/renderers/sequence/README.md +114 -0
- graphitect/_vendor/archify/renderers/sequence/render-sequence.mjs +464 -0
- graphitect/_vendor/archify/renderers/shared/brand-marks.mjs +563 -0
- graphitect/_vendor/archify/renderers/shared/cli.mjs +218 -0
- graphitect/_vendor/archify/renderers/shared/desktop-readability.mjs +26 -0
- graphitect/_vendor/archify/renderers/shared/diagnostics.mjs +127 -0
- graphitect/_vendor/archify/renderers/shared/engineering-profiles.mjs +157 -0
- graphitect/_vendor/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
- graphitect/_vendor/archify/renderers/shared/generated-validators.mjs +13 -0
- graphitect/_vendor/archify/renderers/shared/geometry.mjs +1423 -0
- graphitect/_vendor/archify/renderers/shared/i18n.mjs +595 -0
- graphitect/_vendor/archify/renderers/shared/layout-report.mjs +40 -0
- graphitect/_vendor/archify/renderers/shared/legend.mjs +217 -0
- graphitect/_vendor/archify/renderers/shared/output-path.mjs +340 -0
- graphitect/_vendor/archify/renderers/shared/repository-evidence.mjs +238 -0
- graphitect/_vendor/archify/renderers/shared/repository-location.mjs +58 -0
- graphitect/_vendor/archify/renderers/shared/text-fit.mjs +49 -0
- graphitect/_vendor/archify/renderers/shared/utils.mjs +232 -0
- graphitect/_vendor/archify/renderers/shared/validator.mjs +86 -0
- graphitect/_vendor/archify/renderers/workflow/README.md +223 -0
- graphitect/_vendor/archify/renderers/workflow/render-workflow.mjs +35 -0
- graphitect/_vendor/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
- graphitect/_vendor/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
- graphitect/_vendor/archify/schemas/README.md +211 -0
- graphitect/_vendor/archify/schemas/architecture.schema.json +178 -0
- graphitect/_vendor/archify/schemas/common.schema.json +115 -0
- graphitect/_vendor/archify/schemas/dataflow.schema.json +243 -0
- graphitect/_vendor/archify/schemas/lifecycle.schema.json +266 -0
- graphitect/_vendor/archify/schemas/sequence.schema.json +223 -0
- graphitect/_vendor/archify/schemas/workflow.schema.json +428 -0
- graphitect/_vendor/archify/scripts/check-render-output.mjs +836 -0
- graphitect/_vendor/archify/scripts/check-update.mjs +1667 -0
- graphitect/_vendor/archify/scripts/generate-brand-marks.mjs +141 -0
- graphitect/_vendor/archify/scripts/generate-validators.mjs +66 -0
- graphitect/_vendor/archify/scripts/render-examples.mjs +26 -0
- graphitect/_vendor/archify/scripts/update-contract.mjs +182 -0
- graphitect/_vendor/archify/skill-release.json +10 -0
- graphitect/cli.py +981 -0
- graphitect/deliver/__init__.py +5 -0
- graphitect/deliver/archify_adapter.py +1877 -0
- graphitect/deliver/archify_ir.py +160 -0
- graphitect/deliver/archify_repair.py +135 -0
- graphitect/deliver/doc_compiler.py +916 -0
- graphitect/ground/__init__.py +5 -0
- graphitect/ground/describe_source.py +27 -0
- graphitect/ground/fullread_source.py +56 -0
- graphitect/ground/graphify_source.py +107 -0
- graphitect/models.py +118 -0
- graphitect/skill/SKILL.md +80 -0
- graphitect/skill/agents/openai.yaml +4 -0
- graphitect/synthesize/__init__.py +5 -0
- graphitect/synthesize/engine.py +281 -0
- graphitect/synthesize/llm_backend.py +331 -0
- graphitect/synthesize/questions.py +139 -0
- graphitect/synthesize/rubric.py +104 -0
- graphitect-0.2.0.dist-info/METADATA +284 -0
- graphitect-0.2.0.dist-info/RECORD +336 -0
- graphitect-0.2.0.dist-info/WHEEL +5 -0
- graphitect-0.2.0.dist-info/entry_points.txt +2 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE +21 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE-ARCHIFY-MIT +22 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE-GRAPHIFY-APACHE-2.0 +202 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE-GRAPHIFY-MIT +21 -0
- graphitect-0.2.0.dist-info/licenses/NOTICE-ARCHIFY-THIRD-PARTY.md +69 -0
- graphitect-0.2.0.dist-info/licenses/NOTICE-GRAPHIFY +8 -0
- graphitect-0.2.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"""Rust extractor. Moved verbatim from graphify/extract.py."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from graphify.extractors.base import _LANGUAGE_BUILTIN_GLOBALS, _file_stem, _make_id, _read_text
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _rust_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None:
|
|
10
|
+
"""Walk a Rust type expression; append (name, role) tuples."""
|
|
11
|
+
if node is None:
|
|
12
|
+
return
|
|
13
|
+
t = node.type
|
|
14
|
+
if t == "primitive_type":
|
|
15
|
+
return
|
|
16
|
+
if t == "type_identifier":
|
|
17
|
+
text = _read_text(node, source)
|
|
18
|
+
if text:
|
|
19
|
+
out.append((text, "generic_arg" if generic else "type"))
|
|
20
|
+
return
|
|
21
|
+
if t == "scoped_type_identifier":
|
|
22
|
+
text = _read_text(node, source).rsplit("::", 1)[-1]
|
|
23
|
+
if text:
|
|
24
|
+
out.append((text, "generic_arg" if generic else "type"))
|
|
25
|
+
return
|
|
26
|
+
if t == "generic_type":
|
|
27
|
+
name_node = node.child_by_field_name("type")
|
|
28
|
+
if name_node is None:
|
|
29
|
+
for c in node.children:
|
|
30
|
+
if c.type in ("type_identifier", "scoped_type_identifier"):
|
|
31
|
+
name_node = c
|
|
32
|
+
break
|
|
33
|
+
if name_node is not None:
|
|
34
|
+
text = _read_text(name_node, source).rsplit("::", 1)[-1]
|
|
35
|
+
if text:
|
|
36
|
+
out.append((text, "generic_arg" if generic else "type"))
|
|
37
|
+
for c in node.children:
|
|
38
|
+
if c.type == "type_arguments":
|
|
39
|
+
for arg in c.children:
|
|
40
|
+
if arg.is_named:
|
|
41
|
+
_rust_collect_type_refs(arg, source, True, out)
|
|
42
|
+
return
|
|
43
|
+
if t in ("reference_type", "pointer_type", "array_type", "tuple_type", "slice_type"):
|
|
44
|
+
for c in node.children:
|
|
45
|
+
if c.is_named:
|
|
46
|
+
_rust_collect_type_refs(c, source, generic, out)
|
|
47
|
+
return
|
|
48
|
+
if node.is_named:
|
|
49
|
+
for c in node.children:
|
|
50
|
+
if c.is_named:
|
|
51
|
+
_rust_collect_type_refs(c, source, generic, out)
|
|
52
|
+
|
|
53
|
+
_RUST_TRAIT_METHOD_BLOCKLIST: frozenset[str] = frozenset({
|
|
54
|
+
"new", "default", "parse", "from_str", "now", "clone", "into", "from",
|
|
55
|
+
"to_string", "to_owned", "len", "is_empty", "iter", "next", "build",
|
|
56
|
+
"start", "run", "init", "app", "get", "set", "push", "pop", "insert",
|
|
57
|
+
"remove", "contains", "collect", "map", "filter", "unwrap", "expect",
|
|
58
|
+
"ok", "err", "some", "none", "send", "recv", "lock", "read", "write",
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
def extract_rust(path: Path) -> dict:
|
|
62
|
+
"""Extract functions, structs, enums, traits, impl methods, statics/consts, and use declarations from a .rs file."""
|
|
63
|
+
try:
|
|
64
|
+
import tree_sitter_rust as tsrust
|
|
65
|
+
from tree_sitter import Language, Parser
|
|
66
|
+
except ImportError:
|
|
67
|
+
return {"nodes": [], "edges": [], "error": "tree-sitter-rust not installed"}
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
language = Language(tsrust.language())
|
|
71
|
+
parser = Parser(language)
|
|
72
|
+
source = path.read_bytes()
|
|
73
|
+
tree = parser.parse(source)
|
|
74
|
+
root = tree.root_node
|
|
75
|
+
except Exception as e:
|
|
76
|
+
return {"nodes": [], "edges": [], "error": str(e)}
|
|
77
|
+
|
|
78
|
+
stem = _file_stem(path)
|
|
79
|
+
str_path = str(path)
|
|
80
|
+
nodes: list[dict] = []
|
|
81
|
+
edges: list[dict] = []
|
|
82
|
+
seen_ids: set[str] = set()
|
|
83
|
+
function_bodies: list[tuple[str, object]] = []
|
|
84
|
+
|
|
85
|
+
def add_node(nid: str, label: str, line: int) -> None:
|
|
86
|
+
if nid not in seen_ids:
|
|
87
|
+
seen_ids.add(nid)
|
|
88
|
+
nodes.append({
|
|
89
|
+
"id": nid,
|
|
90
|
+
"label": label,
|
|
91
|
+
"file_type": "code",
|
|
92
|
+
"source_file": str_path,
|
|
93
|
+
"source_location": f"L{line}",
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
def add_edge(src: str, tgt: str, relation: str, line: int,
|
|
97
|
+
confidence: str = "EXTRACTED", weight: float = 1.0,
|
|
98
|
+
context: str | None = None) -> None:
|
|
99
|
+
edge = {
|
|
100
|
+
"source": src,
|
|
101
|
+
"target": tgt,
|
|
102
|
+
"relation": relation,
|
|
103
|
+
"confidence": confidence,
|
|
104
|
+
"source_file": str_path,
|
|
105
|
+
"source_location": f"L{line}",
|
|
106
|
+
"weight": weight,
|
|
107
|
+
}
|
|
108
|
+
if context:
|
|
109
|
+
edge["context"] = context
|
|
110
|
+
edges.append(edge)
|
|
111
|
+
|
|
112
|
+
file_nid = _make_id(str(path))
|
|
113
|
+
add_node(file_nid, path.name, 1)
|
|
114
|
+
|
|
115
|
+
def ensure_named_node(name: str, line: int) -> str:
|
|
116
|
+
nid = _make_id(stem, name)
|
|
117
|
+
if nid in seen_ids:
|
|
118
|
+
return nid
|
|
119
|
+
nid = _make_id(name)
|
|
120
|
+
if nid not in seen_ids:
|
|
121
|
+
# The name isn't defined in this file, so this is a cross-file reference
|
|
122
|
+
# (e.g. a `Thing` type annotation imported from another module). Emit a
|
|
123
|
+
# SOURCELESS stub — like the inheritance-base path below — so the
|
|
124
|
+
# corpus-level rewire can collapse it onto the real definition. A sourced
|
|
125
|
+
# stub here makes _disambiguate_colliding_node_ids bake the referencing
|
|
126
|
+
# file's path (with extension) into the id and blocks the rewire, which is
|
|
127
|
+
# the phantom-duplicate-node bug (#1402).
|
|
128
|
+
seen_ids.add(nid)
|
|
129
|
+
nodes.append({
|
|
130
|
+
"id": nid,
|
|
131
|
+
"label": name,
|
|
132
|
+
"file_type": "code",
|
|
133
|
+
"source_file": "",
|
|
134
|
+
"source_location": "",
|
|
135
|
+
"origin_file": str_path,
|
|
136
|
+
})
|
|
137
|
+
return nid
|
|
138
|
+
|
|
139
|
+
def emit_param_return_refs(func_node, func_nid: str, line: int) -> None:
|
|
140
|
+
params = func_node.child_by_field_name("parameters")
|
|
141
|
+
if params is not None:
|
|
142
|
+
for p in params.children:
|
|
143
|
+
if p.type != "parameter":
|
|
144
|
+
continue
|
|
145
|
+
type_node = p.child_by_field_name("type")
|
|
146
|
+
refs: list[tuple[str, str]] = []
|
|
147
|
+
_rust_collect_type_refs(type_node, source, False, refs)
|
|
148
|
+
for ref_name, role in refs:
|
|
149
|
+
ctx = "generic_arg" if role == "generic_arg" else "parameter_type"
|
|
150
|
+
tgt = ensure_named_node(ref_name, line)
|
|
151
|
+
if tgt != func_nid:
|
|
152
|
+
add_edge(func_nid, tgt, "references", line, context=ctx)
|
|
153
|
+
return_type = func_node.child_by_field_name("return_type")
|
|
154
|
+
if return_type is not None:
|
|
155
|
+
refs = []
|
|
156
|
+
_rust_collect_type_refs(return_type, source, False, refs)
|
|
157
|
+
for ref_name, role in refs:
|
|
158
|
+
ctx = "generic_arg" if role == "generic_arg" else "return_type"
|
|
159
|
+
tgt = ensure_named_node(ref_name, line)
|
|
160
|
+
if tgt != func_nid:
|
|
161
|
+
add_edge(func_nid, tgt, "references", line, context=ctx)
|
|
162
|
+
|
|
163
|
+
def walk(node, parent_impl_nid: str | None = None) -> None:
|
|
164
|
+
t = node.type
|
|
165
|
+
|
|
166
|
+
if t == "function_item":
|
|
167
|
+
name_node = node.child_by_field_name("name")
|
|
168
|
+
if name_node:
|
|
169
|
+
func_name = _read_text(name_node, source)
|
|
170
|
+
line = node.start_point[0] + 1
|
|
171
|
+
if parent_impl_nid:
|
|
172
|
+
func_nid = _make_id(parent_impl_nid, func_name)
|
|
173
|
+
add_node(func_nid, f".{func_name}()", line)
|
|
174
|
+
add_edge(parent_impl_nid, func_nid, "method", line)
|
|
175
|
+
else:
|
|
176
|
+
func_nid = _make_id(stem, func_name)
|
|
177
|
+
add_node(func_nid, f"{func_name}()", line)
|
|
178
|
+
add_edge(file_nid, func_nid, "contains", line)
|
|
179
|
+
emit_param_return_refs(node, func_nid, line)
|
|
180
|
+
body = node.child_by_field_name("body")
|
|
181
|
+
if body:
|
|
182
|
+
function_bodies.append((func_nid, body))
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
if t == "function_signature_item":
|
|
186
|
+
# `fn greet(&self) -> String;` — a trait method with no body. This node type
|
|
187
|
+
# had no branch at all, so a trait's required methods were unreachable: the
|
|
188
|
+
# only method nodes in a graph came from impl blocks, and a trait with no
|
|
189
|
+
# implementor in the corpus contributed none. Mirrors function_item, minus
|
|
190
|
+
# the body walk (there is no body).
|
|
191
|
+
name_node = node.child_by_field_name("name")
|
|
192
|
+
if name_node:
|
|
193
|
+
func_name = _read_text(name_node, source)
|
|
194
|
+
line = node.start_point[0] + 1
|
|
195
|
+
if parent_impl_nid:
|
|
196
|
+
func_nid = _make_id(parent_impl_nid, func_name)
|
|
197
|
+
add_node(func_nid, f".{func_name}()", line)
|
|
198
|
+
add_edge(parent_impl_nid, func_nid, "method", line)
|
|
199
|
+
else:
|
|
200
|
+
func_nid = _make_id(stem, func_name)
|
|
201
|
+
add_node(func_nid, f"{func_name}()", line)
|
|
202
|
+
add_edge(file_nid, func_nid, "contains", line)
|
|
203
|
+
emit_param_return_refs(node, func_nid, line)
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
if t in ("struct_item", "enum_item", "trait_item"):
|
|
207
|
+
name_node = node.child_by_field_name("name")
|
|
208
|
+
if name_node:
|
|
209
|
+
item_name = _read_text(name_node, source)
|
|
210
|
+
line = node.start_point[0] + 1
|
|
211
|
+
item_nid = _make_id(stem, item_name)
|
|
212
|
+
add_node(item_nid, item_name, line)
|
|
213
|
+
add_edge(file_nid, item_nid, "contains", line)
|
|
214
|
+
if t == "trait_item":
|
|
215
|
+
for c in node.children:
|
|
216
|
+
if c.type != "trait_bounds":
|
|
217
|
+
continue
|
|
218
|
+
for sub in c.children:
|
|
219
|
+
if not sub.is_named:
|
|
220
|
+
continue
|
|
221
|
+
refs: list[tuple[str, str]] = []
|
|
222
|
+
_rust_collect_type_refs(sub, source, False, refs)
|
|
223
|
+
for idx, (ref_name, _role) in enumerate(refs):
|
|
224
|
+
tgt = ensure_named_node(ref_name, line)
|
|
225
|
+
if tgt == item_nid:
|
|
226
|
+
continue
|
|
227
|
+
rel = "inherits" if idx == 0 else "references"
|
|
228
|
+
if rel == "inherits":
|
|
229
|
+
add_edge(item_nid, tgt, "inherits", line)
|
|
230
|
+
else:
|
|
231
|
+
add_edge(item_nid, tgt, "references", line,
|
|
232
|
+
context="generic_arg")
|
|
233
|
+
if t == "struct_item":
|
|
234
|
+
for c in node.children:
|
|
235
|
+
if c.type != "field_declaration_list":
|
|
236
|
+
continue
|
|
237
|
+
for field in c.children:
|
|
238
|
+
if field.type != "field_declaration":
|
|
239
|
+
continue
|
|
240
|
+
type_node = field.child_by_field_name("type")
|
|
241
|
+
if type_node is None:
|
|
242
|
+
for fc in field.children:
|
|
243
|
+
if fc.type in ("type_identifier", "generic_type",
|
|
244
|
+
"scoped_type_identifier",
|
|
245
|
+
"reference_type", "primitive_type"):
|
|
246
|
+
type_node = fc
|
|
247
|
+
break
|
|
248
|
+
refs = []
|
|
249
|
+
_rust_collect_type_refs(type_node, source, False, refs)
|
|
250
|
+
for ref_name, role in refs:
|
|
251
|
+
ctx = "generic_arg" if role == "generic_arg" else "field"
|
|
252
|
+
tgt = ensure_named_node(ref_name, field.start_point[0] + 1)
|
|
253
|
+
if tgt != item_nid:
|
|
254
|
+
add_edge(item_nid, tgt, "references",
|
|
255
|
+
field.start_point[0] + 1, context=ctx)
|
|
256
|
+
# Tuple structs (`struct Wrapper(pub Logger, Config);`) nest their
|
|
257
|
+
# positional field types directly under ordered_field_declaration_list
|
|
258
|
+
# with no field_declaration wrapper -- the same shape handled for tuple
|
|
259
|
+
# enum variants below. Without this branch these field type references
|
|
260
|
+
# are silently dropped.
|
|
261
|
+
for c in node.children:
|
|
262
|
+
if c.type != "ordered_field_declaration_list":
|
|
263
|
+
continue
|
|
264
|
+
fline = c.start_point[0] + 1
|
|
265
|
+
for tc in c.children:
|
|
266
|
+
if tc.type not in ("type_identifier", "generic_type",
|
|
267
|
+
"scoped_type_identifier", "reference_type",
|
|
268
|
+
"primitive_type", "tuple_type", "array_type"):
|
|
269
|
+
continue
|
|
270
|
+
refs = []
|
|
271
|
+
_rust_collect_type_refs(tc, source, False, refs)
|
|
272
|
+
for ref_name, role in refs:
|
|
273
|
+
ctx = "generic_arg" if role == "generic_arg" else "field"
|
|
274
|
+
tgt = ensure_named_node(ref_name, fline)
|
|
275
|
+
if tgt != item_nid:
|
|
276
|
+
add_edge(item_nid, tgt, "references", fline, context=ctx)
|
|
277
|
+
if t == "enum_item":
|
|
278
|
+
# Variant payload types nest under enum_variant_list ->
|
|
279
|
+
# enum_variant -> ordered_field_declaration_list (tuple variant,
|
|
280
|
+
# `Click(Logger)`) | field_declaration_list (struct variant,
|
|
281
|
+
# `Resize { size: Dim }`). Neither was traversed, so every
|
|
282
|
+
# enum-variant type reference was silently dropped.
|
|
283
|
+
_TYPE_NODES = ("type_identifier", "generic_type",
|
|
284
|
+
"scoped_type_identifier", "reference_type",
|
|
285
|
+
"primitive_type", "tuple_type", "array_type")
|
|
286
|
+
|
|
287
|
+
def _emit_enum_type(type_node, at_line):
|
|
288
|
+
if type_node is None:
|
|
289
|
+
return
|
|
290
|
+
refs2: list[tuple[str, str]] = []
|
|
291
|
+
_rust_collect_type_refs(type_node, source, False, refs2)
|
|
292
|
+
for ref_name, role in refs2:
|
|
293
|
+
ctx = "generic_arg" if role == "generic_arg" else "field"
|
|
294
|
+
tgt = ensure_named_node(ref_name, at_line)
|
|
295
|
+
if tgt != item_nid:
|
|
296
|
+
add_edge(item_nid, tgt, "references", at_line, context=ctx)
|
|
297
|
+
|
|
298
|
+
for c in node.children:
|
|
299
|
+
if c.type != "enum_variant_list":
|
|
300
|
+
continue
|
|
301
|
+
for variant in c.children:
|
|
302
|
+
if variant.type != "enum_variant":
|
|
303
|
+
continue
|
|
304
|
+
vline = variant.start_point[0] + 1
|
|
305
|
+
for vc in variant.children:
|
|
306
|
+
if vc.type == "ordered_field_declaration_list":
|
|
307
|
+
for tc in vc.children:
|
|
308
|
+
if tc.type in _TYPE_NODES:
|
|
309
|
+
_emit_enum_type(tc, vline)
|
|
310
|
+
elif vc.type == "field_declaration_list":
|
|
311
|
+
for field in vc.children:
|
|
312
|
+
if field.type != "field_declaration":
|
|
313
|
+
continue
|
|
314
|
+
type_node = field.child_by_field_name("type")
|
|
315
|
+
_emit_enum_type(type_node, field.start_point[0] + 1)
|
|
316
|
+
if t == "trait_item":
|
|
317
|
+
# The methods a trait declares are its contract, and they were not
|
|
318
|
+
# reached: this branch returns below, so nothing walked the body.
|
|
319
|
+
# Descend the same way impl_item does, attributing each method to
|
|
320
|
+
# the trait node — so `explain <Trait>` can list what an
|
|
321
|
+
# implementor must provide.
|
|
322
|
+
body = node.child_by_field_name("body")
|
|
323
|
+
if body:
|
|
324
|
+
for child in body.children:
|
|
325
|
+
walk(child, parent_impl_nid=item_nid)
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
if t in ("static_item", "const_item"):
|
|
329
|
+
# `static NAME: T = …;` / `const NAME: T = …;` at module level, or an
|
|
330
|
+
# associated const inside an impl. Neither node type had a branch, so a
|
|
331
|
+
# constant reached the graph only through files that referenced it,
|
|
332
|
+
# never from the Rust that defines it (#3471).
|
|
333
|
+
name_node = node.child_by_field_name("name")
|
|
334
|
+
if name_node:
|
|
335
|
+
item_name = _read_text(name_node, source)
|
|
336
|
+
line = node.start_point[0] + 1
|
|
337
|
+
if parent_impl_nid:
|
|
338
|
+
item_nid = _make_id(parent_impl_nid, item_name)
|
|
339
|
+
add_node(item_nid, f".{item_name}", line)
|
|
340
|
+
add_edge(parent_impl_nid, item_nid, "contains", line)
|
|
341
|
+
else:
|
|
342
|
+
item_nid = _make_id(stem, item_name)
|
|
343
|
+
add_node(item_nid, item_name, line)
|
|
344
|
+
add_edge(file_nid, item_nid, "contains", line)
|
|
345
|
+
type_node = node.child_by_field_name("type")
|
|
346
|
+
if type_node is not None:
|
|
347
|
+
refs: list[tuple[str, str]] = []
|
|
348
|
+
_rust_collect_type_refs(type_node, source, False, refs)
|
|
349
|
+
for ref_name, role in refs:
|
|
350
|
+
tgt = ensure_named_node(ref_name, line)
|
|
351
|
+
if tgt == item_nid:
|
|
352
|
+
continue
|
|
353
|
+
ctx = "generic_arg" if role == "generic_arg" else "field"
|
|
354
|
+
add_edge(item_nid, tgt, "references", line, context=ctx)
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
if t == "impl_item":
|
|
358
|
+
type_node = node.child_by_field_name("type")
|
|
359
|
+
trait_node = node.child_by_field_name("trait")
|
|
360
|
+
impl_nid: str | None = None
|
|
361
|
+
if type_node:
|
|
362
|
+
type_name = _read_text(type_node, source).strip()
|
|
363
|
+
impl_nid = _make_id(stem, type_name)
|
|
364
|
+
add_node(impl_nid, type_name, node.start_point[0] + 1)
|
|
365
|
+
if trait_node is not None and impl_nid is not None:
|
|
366
|
+
refs: list[tuple[str, str]] = []
|
|
367
|
+
_rust_collect_type_refs(trait_node, source, False, refs)
|
|
368
|
+
for idx, (ref_name, _role) in enumerate(refs):
|
|
369
|
+
tgt = ensure_named_node(ref_name, node.start_point[0] + 1)
|
|
370
|
+
if tgt == impl_nid:
|
|
371
|
+
continue
|
|
372
|
+
if idx == 0:
|
|
373
|
+
add_edge(impl_nid, tgt, "implements", node.start_point[0] + 1)
|
|
374
|
+
else:
|
|
375
|
+
add_edge(impl_nid, tgt, "references", node.start_point[0] + 1,
|
|
376
|
+
context="generic_arg")
|
|
377
|
+
body = node.child_by_field_name("body")
|
|
378
|
+
if body:
|
|
379
|
+
for child in body.children:
|
|
380
|
+
walk(child, parent_impl_nid=impl_nid)
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
if t == "use_declaration":
|
|
384
|
+
arg = node.child_by_field_name("argument")
|
|
385
|
+
if arg:
|
|
386
|
+
raw = _read_text(arg, source)
|
|
387
|
+
clean = raw.split("{")[0].rstrip(":").rstrip("*").rstrip(":")
|
|
388
|
+
module_name = clean.split("::")[-1].strip()
|
|
389
|
+
if module_name:
|
|
390
|
+
tgt_nid = _make_id(module_name)
|
|
391
|
+
add_edge(file_nid, tgt_nid, "imports_from", node.start_point[0] + 1, context="import")
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
for child in node.children:
|
|
395
|
+
walk(child, parent_impl_nid=None)
|
|
396
|
+
|
|
397
|
+
walk(root)
|
|
398
|
+
|
|
399
|
+
label_to_nid: dict[str, str] = {}
|
|
400
|
+
for n in nodes:
|
|
401
|
+
raw = n["label"]
|
|
402
|
+
normalised = raw.strip("()").lstrip(".")
|
|
403
|
+
label_to_nid[normalised] = n["id"]
|
|
404
|
+
|
|
405
|
+
seen_call_pairs: set[tuple[str, str]] = set()
|
|
406
|
+
raw_calls: list[dict] = []
|
|
407
|
+
|
|
408
|
+
def walk_calls(node, caller_nid: str) -> None:
|
|
409
|
+
if node.type == "function_item":
|
|
410
|
+
return
|
|
411
|
+
if node.type == "call_expression":
|
|
412
|
+
func_node = node.child_by_field_name("function")
|
|
413
|
+
callee_name: str | None = None
|
|
414
|
+
is_member_call: bool = False
|
|
415
|
+
is_scoped_call: bool = False
|
|
416
|
+
if func_node:
|
|
417
|
+
if func_node.type == "identifier":
|
|
418
|
+
callee_name = _read_text(func_node, source)
|
|
419
|
+
elif func_node.type == "field_expression":
|
|
420
|
+
is_member_call = True
|
|
421
|
+
field = func_node.child_by_field_name("field")
|
|
422
|
+
if field:
|
|
423
|
+
callee_name = _read_text(field, source)
|
|
424
|
+
elif func_node.type == "scoped_identifier":
|
|
425
|
+
# Type::method() — still allow in-file EXTRACTED match, but
|
|
426
|
+
# skip cross-file resolution: bare last-segment lookup ignores
|
|
427
|
+
# crate boundaries and produces spurious INFERRED edges (#908).
|
|
428
|
+
is_scoped_call = True
|
|
429
|
+
name = func_node.child_by_field_name("name")
|
|
430
|
+
if name:
|
|
431
|
+
callee_name = _read_text(name, source)
|
|
432
|
+
if callee_name and callee_name not in _LANGUAGE_BUILTIN_GLOBALS:
|
|
433
|
+
tgt_nid = label_to_nid.get(callee_name)
|
|
434
|
+
if tgt_nid and tgt_nid != caller_nid:
|
|
435
|
+
pair = (caller_nid, tgt_nid)
|
|
436
|
+
if pair not in seen_call_pairs:
|
|
437
|
+
seen_call_pairs.add(pair)
|
|
438
|
+
line = node.start_point[0] + 1
|
|
439
|
+
edges.append({
|
|
440
|
+
"source": caller_nid,
|
|
441
|
+
"target": tgt_nid,
|
|
442
|
+
"relation": "calls",
|
|
443
|
+
"context": "call",
|
|
444
|
+
"confidence": "EXTRACTED",
|
|
445
|
+
"source_file": str_path,
|
|
446
|
+
"source_location": f"L{line}",
|
|
447
|
+
"weight": 1.0,
|
|
448
|
+
})
|
|
449
|
+
elif not is_scoped_call and callee_name.lower() not in _RUST_TRAIT_METHOD_BLOCKLIST:
|
|
450
|
+
raw_calls.append({
|
|
451
|
+
"caller_nid": caller_nid,
|
|
452
|
+
"callee": callee_name,
|
|
453
|
+
"is_member_call": is_member_call,
|
|
454
|
+
"source_file": str_path,
|
|
455
|
+
"source_location": f"L{node.start_point[0] + 1}",
|
|
456
|
+
})
|
|
457
|
+
for child in node.children:
|
|
458
|
+
walk_calls(child, caller_nid)
|
|
459
|
+
|
|
460
|
+
for caller_nid, body_node in function_bodies:
|
|
461
|
+
walk_calls(body_node, caller_nid)
|
|
462
|
+
|
|
463
|
+
valid_ids = seen_ids
|
|
464
|
+
clean_edges = []
|
|
465
|
+
for edge in edges:
|
|
466
|
+
src, tgt = edge["source"], edge["target"]
|
|
467
|
+
if src in valid_ids and (tgt in valid_ids or edge["relation"] in ("imports", "imports_from")):
|
|
468
|
+
clean_edges.append(edge)
|
|
469
|
+
|
|
470
|
+
return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Sln extractor. Moved verbatim from graphify/extract.py."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from graphify.extractors.base import _make_id
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def extract_sln(path: Path) -> dict:
|
|
11
|
+
"""Extract projects and inter-project dependencies from a .sln file."""
|
|
12
|
+
try:
|
|
13
|
+
src = path.read_text(encoding="utf-8", errors="replace")
|
|
14
|
+
except OSError:
|
|
15
|
+
return {"nodes": [], "edges": [], "error": f"cannot read {path}"}
|
|
16
|
+
|
|
17
|
+
file_nid = _make_id(str(path))
|
|
18
|
+
str_path = str(path)
|
|
19
|
+
nodes: list[dict] = [{"id": file_nid, "label": path.name, "file_type": "code",
|
|
20
|
+
"source_file": str_path, "source_location": None}]
|
|
21
|
+
edges: list[dict] = []
|
|
22
|
+
seen_ids: set[str] = set()
|
|
23
|
+
seen_ids.add(file_nid)
|
|
24
|
+
|
|
25
|
+
_PROJECT_RE = re.compile(
|
|
26
|
+
r'Project\("[^"]*"\)\s*=\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]*)"'
|
|
27
|
+
)
|
|
28
|
+
_DEP_RE = re.compile(r'\{([0-9a-fA-F-]+)\}\s*=\s*\{([0-9a-fA-F-]+)\}')
|
|
29
|
+
|
|
30
|
+
guid_to_nid: dict[str, str] = {}
|
|
31
|
+
|
|
32
|
+
for m in _PROJECT_RE.finditer(src):
|
|
33
|
+
proj_name = m.group(1)
|
|
34
|
+
proj_path = m.group(2).replace("\\", "/")
|
|
35
|
+
proj_guid = m.group(3).strip("{}")
|
|
36
|
+
|
|
37
|
+
# A solution folder is a VIRTUAL grouping, not a file: Visual Studio writes
|
|
38
|
+
# its name as both the display name and the "path" (proj_name == proj_path,
|
|
39
|
+
# no real file). Resolving it to an absolute path and keying the node id off
|
|
40
|
+
# that leaked the absolute scan path (incl. the OS username) into graph.json,
|
|
41
|
+
# because the CLI's id-relativization only remaps ids of real files in the
|
|
42
|
+
# scan set — a virtual folder never matches, so its absolute id survived
|
|
43
|
+
# (#1789). Use the folder name itself (relative, no filesystem resolution).
|
|
44
|
+
is_solution_folder = proj_path == proj_name
|
|
45
|
+
if is_solution_folder:
|
|
46
|
+
abs_proj = proj_name
|
|
47
|
+
else:
|
|
48
|
+
try:
|
|
49
|
+
abs_proj = str((path.parent / proj_path).resolve())
|
|
50
|
+
except Exception:
|
|
51
|
+
abs_proj = proj_path
|
|
52
|
+
proj_nid = _make_id(abs_proj)
|
|
53
|
+
if proj_nid and proj_nid not in seen_ids:
|
|
54
|
+
seen_ids.add(proj_nid)
|
|
55
|
+
nodes.append({"id": proj_nid, "label": proj_name,
|
|
56
|
+
"file_type": "code", "source_file": abs_proj,
|
|
57
|
+
"source_location": None})
|
|
58
|
+
edges.append({"source": file_nid, "target": proj_nid,
|
|
59
|
+
"relation": "contains", "confidence": "EXTRACTED",
|
|
60
|
+
"source_file": str_path, "weight": 1.0})
|
|
61
|
+
if proj_guid:
|
|
62
|
+
guid_to_nid[proj_guid.lower()] = proj_nid
|
|
63
|
+
|
|
64
|
+
in_dep_section = False
|
|
65
|
+
current_proj_guid: str | None = None
|
|
66
|
+
_PROJECT_LINE_RE = re.compile(r'Project\("[^"]*"\)\s*=\s*"[^"]+"\s*,\s*"[^"]+"\s*,\s*"\{([^}]+)\}"')
|
|
67
|
+
for line in src.splitlines():
|
|
68
|
+
proj_line_m = _PROJECT_LINE_RE.search(line)
|
|
69
|
+
if proj_line_m:
|
|
70
|
+
current_proj_guid = proj_line_m.group(1).lower()
|
|
71
|
+
continue
|
|
72
|
+
if line.strip() == "EndProject":
|
|
73
|
+
current_proj_guid = None
|
|
74
|
+
continue
|
|
75
|
+
if "ProjectSection(ProjectDependencies)" in line:
|
|
76
|
+
in_dep_section = True
|
|
77
|
+
continue
|
|
78
|
+
if in_dep_section and "EndProjectSection" in line:
|
|
79
|
+
in_dep_section = False
|
|
80
|
+
continue
|
|
81
|
+
if in_dep_section and current_proj_guid:
|
|
82
|
+
dep_m = _DEP_RE.search(line)
|
|
83
|
+
if dep_m:
|
|
84
|
+
to_guid = dep_m.group(1).lower()
|
|
85
|
+
from_nid = guid_to_nid.get(current_proj_guid)
|
|
86
|
+
to_nid = guid_to_nid.get(to_guid)
|
|
87
|
+
if from_nid and to_nid and from_nid != to_nid:
|
|
88
|
+
edges.append({"source": from_nid, "target": to_nid,
|
|
89
|
+
"relation": "imports", "confidence": "EXTRACTED",
|
|
90
|
+
"source_file": str_path, "weight": 1.0})
|
|
91
|
+
|
|
92
|
+
return {"nodes": nodes, "edges": edges}
|