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,296 @@
|
|
|
1
|
+
"""Robot Framework extractor for .robot/.resource files (issue #3192)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from graphify.extractors.base import _file_stem, _make_id
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Robot Framework standard libraries - imports of these are noise (like the
|
|
12
|
+
# Python built-ins in _LANGUAGE_BUILTIN_GLOBALS), so only third-party
|
|
13
|
+
# libraries get stub nodes.
|
|
14
|
+
_ROBOT_STDLIBS = frozenset({
|
|
15
|
+
"BuiltIn", "Collections", "DateTime", "Dialogs", "Easter", "OperatingSystem",
|
|
16
|
+
"Process", "Remote", "Reserved", "Screenshot", "String", "Telnet", "XML",
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_ROBOT_VAR_RE = re.compile(r"\$\{([^}]*)\}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _resolve_robot_import(raw: str, source_path: Path) -> Path | None:
|
|
24
|
+
"""Resolve a Settings-section import path relative to the importing file.
|
|
25
|
+
|
|
26
|
+
Preserves the relative/absolute form of source_path so the target ID
|
|
27
|
+
matches the ID the imported file's own extraction produces (same approach
|
|
28
|
+
as the JS relative-import resolver). Robot matches variable names case-,
|
|
29
|
+
space-, and underscore-insensitively, so ``${curdir}`` and ``${Cur_Dir}``
|
|
30
|
+
resolve like ``${CURDIR}``. Returns None when the path holds any other
|
|
31
|
+
(unresolvable) ${VARIABLE} or %{ENV_VAR}.
|
|
32
|
+
"""
|
|
33
|
+
s = raw.strip()
|
|
34
|
+
# ${CURDIR}/${EXECDIR} already anchor the path - substituting and then
|
|
35
|
+
# re-joining below would prefix source_path.parent twice when the scan
|
|
36
|
+
# uses relative paths, so track anchoring explicitly.
|
|
37
|
+
anchored = False
|
|
38
|
+
parts = []
|
|
39
|
+
last = 0
|
|
40
|
+
for m in _ROBOT_VAR_RE.finditer(s):
|
|
41
|
+
name = m.group(1).replace(" ", "").replace("_", "").lower()
|
|
42
|
+
if name == "/":
|
|
43
|
+
repl = "/"
|
|
44
|
+
elif name == "curdir":
|
|
45
|
+
repl = str(source_path.parent)
|
|
46
|
+
anchored = True
|
|
47
|
+
elif name == "execdir":
|
|
48
|
+
# ${EXECDIR} is the directory Robot was launched from - statically
|
|
49
|
+
# knowable only for a relative scan, where "." is the scan root
|
|
50
|
+
# and matches how relative file-node IDs are built. For an
|
|
51
|
+
# absolute source path the resolved ID could never match a node,
|
|
52
|
+
# so emit nothing rather than a guaranteed-dangling edge.
|
|
53
|
+
if source_path.is_absolute():
|
|
54
|
+
return None
|
|
55
|
+
repl = "."
|
|
56
|
+
anchored = True
|
|
57
|
+
else:
|
|
58
|
+
return None
|
|
59
|
+
parts.append(s[last:m.start()])
|
|
60
|
+
parts.append(repl)
|
|
61
|
+
last = m.end()
|
|
62
|
+
parts.append(s[last:])
|
|
63
|
+
s = "".join(parts)
|
|
64
|
+
if "%{" in s:
|
|
65
|
+
return None
|
|
66
|
+
p = Path(s)
|
|
67
|
+
if not anchored and not p.is_absolute():
|
|
68
|
+
p = source_path.parent / p
|
|
69
|
+
# normpath unconditionally so ../ segments collapse and the ID matches
|
|
70
|
+
# the imported file's own node ID (absolute paths included)
|
|
71
|
+
return Path(os.path.normpath(p))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Robot resolves BDD-style calls (Given/When/Then/And/But <keyword>) by trying
|
|
75
|
+
# the full name first, then the name with one prefix stripped. English prefixes
|
|
76
|
+
# only - Robot's localized BDD prefixes are out of scope here.
|
|
77
|
+
_BDD_PREFIXES = ("given ", "when ", "then ", "and ", "but ")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _kw_id(name: str) -> str:
|
|
81
|
+
"""Node ID for a keyword, normalized the way Robot Framework matches
|
|
82
|
+
keyword names: case-, space-, and underscore-insensitive. Stripping
|
|
83
|
+
spaces/underscores before _make_id means ``Open Session``,
|
|
84
|
+
``open_session``, and ``OpenSession`` all share one node ID, exactly as
|
|
85
|
+
Robot resolves them to one keyword."""
|
|
86
|
+
return _make_id(name.replace(" ", "").replace("_", ""))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def extract_robot(path: Path) -> dict:
|
|
90
|
+
"""Extract suites, test cases, user keywords, imports, and keyword calls
|
|
91
|
+
from Robot Framework .robot/.resource files via the official robot.api
|
|
92
|
+
parser (no maintained tree-sitter grammar exists for Robot Framework).
|
|
93
|
+
|
|
94
|
+
Nodes: the suite/resource file, its test cases, and its user keywords.
|
|
95
|
+
Edges: `contains` (file -> test case / keyword), `imports` (file -> the
|
|
96
|
+
Resource/Library/Variables files it pulls in, resolved onto the imported
|
|
97
|
+
file's own node; non-stdlib named libraries like SeleniumLibrary get stub
|
|
98
|
+
nodes so suites sharing a library cluster together), and `calls`
|
|
99
|
+
(test case / keyword / suite fixture -> the keyword it invokes).
|
|
100
|
+
|
|
101
|
+
Keyword nodes are keyed by bare keyword name (not stem-qualified),
|
|
102
|
+
normalized the way Robot matches keywords (case-, space-, and
|
|
103
|
+
underscore-insensitive, see _kw_id): Robot resolves keywords globally by
|
|
104
|
+
name across imported resources, so bare IDs make cross-file call edges
|
|
105
|
+
land on the defining node without a separate resolution pass. Two files
|
|
106
|
+
defining the same keyword name deliberately merge into one node - that
|
|
107
|
+
mirrors Robot's own single global namespace (where such duplicates are
|
|
108
|
+
ambiguous). Test case nodes stay stem-qualified because test names repeat
|
|
109
|
+
across suites. Calls to keywords never defined in the corpus (e.g.
|
|
110
|
+
BuiltIn's Log) are dropped by build_from_json like any external reference.
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
from robot.api import get_model, get_resource_model
|
|
114
|
+
from robot.api.parsing import ModelVisitor
|
|
115
|
+
except ImportError:
|
|
116
|
+
return {"nodes": [], "edges": [],
|
|
117
|
+
"error": "robotframework not installed. Run: pip install robotframework"}
|
|
118
|
+
|
|
119
|
+
stem = _file_stem(path)
|
|
120
|
+
str_path = str(path)
|
|
121
|
+
nodes: list[dict] = []
|
|
122
|
+
edges: list[dict] = []
|
|
123
|
+
seen_ids: set[str] = set()
|
|
124
|
+
seen_edges: set[tuple] = set()
|
|
125
|
+
|
|
126
|
+
def add_node(nid: str, label: str, line: int) -> None:
|
|
127
|
+
if nid not in seen_ids:
|
|
128
|
+
seen_ids.add(nid)
|
|
129
|
+
nodes.append({
|
|
130
|
+
"id": nid,
|
|
131
|
+
"label": label,
|
|
132
|
+
"file_type": "code",
|
|
133
|
+
"source_file": str_path,
|
|
134
|
+
"source_location": f"L{line}",
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
def add_edge(src: str, tgt: str, relation: str, line: int,
|
|
138
|
+
confidence: str = "EXTRACTED", context: str | None = None) -> None:
|
|
139
|
+
if not src or not tgt or src == tgt:
|
|
140
|
+
return
|
|
141
|
+
key = (src, tgt, relation)
|
|
142
|
+
if key in seen_edges:
|
|
143
|
+
return
|
|
144
|
+
seen_edges.add(key)
|
|
145
|
+
edge = {
|
|
146
|
+
"source": src,
|
|
147
|
+
"target": tgt,
|
|
148
|
+
"relation": relation,
|
|
149
|
+
"confidence": confidence,
|
|
150
|
+
"source_file": str_path,
|
|
151
|
+
"source_location": f"L{line}",
|
|
152
|
+
"weight": 1.0,
|
|
153
|
+
}
|
|
154
|
+
if context:
|
|
155
|
+
edge["context"] = context
|
|
156
|
+
edges.append(edge)
|
|
157
|
+
|
|
158
|
+
file_nid = _make_id(str_path)
|
|
159
|
+
add_node(file_nid, path.name, 1)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
if path.suffix.lower() == ".resource":
|
|
163
|
+
model = get_resource_model(str_path)
|
|
164
|
+
else:
|
|
165
|
+
model = get_model(str_path)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
return {"nodes": nodes, "edges": edges, "error": str(e)}
|
|
168
|
+
|
|
169
|
+
def kw_targets(name: str) -> list:
|
|
170
|
+
# "SSHLibrary.Open Connection" -> keyword part only; explicit
|
|
171
|
+
# library/resource prefixes are common, dots inside keyword names are not.
|
|
172
|
+
name = name.rsplit(".", 1)[-1].strip()
|
|
173
|
+
targets = [_kw_id(name)]
|
|
174
|
+
# BDD calls: emit an edge for BOTH the full and the prefix-stripped
|
|
175
|
+
# candidate, mirroring Robot's try-full-then-stripped resolution.
|
|
176
|
+
# Whichever keyword exists receives the edge; the other candidate
|
|
177
|
+
# dangles and is dropped by the graph builder.
|
|
178
|
+
low = name.lower()
|
|
179
|
+
for prefix in _BDD_PREFIXES:
|
|
180
|
+
if low.startswith(prefix) and len(name) > len(prefix):
|
|
181
|
+
targets.append(_kw_id(name[len(prefix):]))
|
|
182
|
+
break
|
|
183
|
+
return targets
|
|
184
|
+
|
|
185
|
+
def add_call_edges(src: str, name: str, line: int) -> None:
|
|
186
|
+
for tgt in kw_targets(name):
|
|
187
|
+
add_edge(src, tgt, "calls", line, context="call")
|
|
188
|
+
|
|
189
|
+
class _RobotVisitor(ModelVisitor):
|
|
190
|
+
def __init__(self):
|
|
191
|
+
self.scope_nid = file_nid
|
|
192
|
+
|
|
193
|
+
# Settings-section imports
|
|
194
|
+
def visit_ResourceImport(self, node):
|
|
195
|
+
raw = (node.name or "").strip()
|
|
196
|
+
if not raw:
|
|
197
|
+
return
|
|
198
|
+
target = _resolve_robot_import(raw, path)
|
|
199
|
+
if target is not None:
|
|
200
|
+
add_edge(file_nid, _make_id(str(target)), "imports", node.lineno)
|
|
201
|
+
|
|
202
|
+
def visit_LibraryImport(self, node):
|
|
203
|
+
raw = (node.name or "").strip()
|
|
204
|
+
if not raw:
|
|
205
|
+
return
|
|
206
|
+
if raw.endswith(".py") or "/" in raw or "\\" in raw:
|
|
207
|
+
# Path-form library import -> edge onto the Python file's node
|
|
208
|
+
target = _resolve_robot_import(raw, path)
|
|
209
|
+
if target is not None:
|
|
210
|
+
add_edge(file_nid, _make_id(str(target)), "imports", node.lineno)
|
|
211
|
+
elif raw not in _ROBOT_STDLIBS:
|
|
212
|
+
# External library (SeleniumLibrary, RequestsLibrary, ...) -
|
|
213
|
+
# stub node so suites sharing a library cluster together.
|
|
214
|
+
# Namespace the id so a user keyword that happens to share the
|
|
215
|
+
# library's name cannot collide with this stub (the label keeps
|
|
216
|
+
# the raw library name for display).
|
|
217
|
+
nid = _make_id("robot_library", raw)
|
|
218
|
+
add_node(nid, raw, node.lineno)
|
|
219
|
+
add_edge(file_nid, nid, "imports", node.lineno)
|
|
220
|
+
|
|
221
|
+
def visit_VariablesImport(self, node):
|
|
222
|
+
raw = (node.name or "").strip()
|
|
223
|
+
if not raw:
|
|
224
|
+
return
|
|
225
|
+
target = _resolve_robot_import(raw, path)
|
|
226
|
+
if target is not None:
|
|
227
|
+
add_edge(file_nid, _make_id(str(target)), "imports", node.lineno)
|
|
228
|
+
|
|
229
|
+
# Suite-level fixtures (file scope)
|
|
230
|
+
def visit_SuiteSetup(self, node):
|
|
231
|
+
if node.name:
|
|
232
|
+
add_call_edges(file_nid, node.name, node.lineno)
|
|
233
|
+
|
|
234
|
+
def visit_SuiteTeardown(self, node):
|
|
235
|
+
if node.name:
|
|
236
|
+
add_call_edges(file_nid, node.name, node.lineno)
|
|
237
|
+
|
|
238
|
+
def visit_TestSetup(self, node):
|
|
239
|
+
if node.name:
|
|
240
|
+
add_call_edges(file_nid, node.name, node.lineno)
|
|
241
|
+
|
|
242
|
+
def visit_TestTeardown(self, node):
|
|
243
|
+
if node.name:
|
|
244
|
+
add_call_edges(file_nid, node.name, node.lineno)
|
|
245
|
+
|
|
246
|
+
def visit_TestTemplate(self, node):
|
|
247
|
+
# Template statements carry the keyword in .value, not .name
|
|
248
|
+
if node.value:
|
|
249
|
+
add_call_edges(file_nid, node.value, node.lineno)
|
|
250
|
+
|
|
251
|
+
# Definitions
|
|
252
|
+
def visit_TestCase(self, node):
|
|
253
|
+
if not node.name:
|
|
254
|
+
return
|
|
255
|
+
tc_nid = _make_id(stem, node.name)
|
|
256
|
+
add_node(tc_nid, node.name, node.lineno)
|
|
257
|
+
add_edge(file_nid, tc_nid, "contains", node.lineno)
|
|
258
|
+
prev, self.scope_nid = self.scope_nid, tc_nid
|
|
259
|
+
self.generic_visit(node)
|
|
260
|
+
self.scope_nid = prev
|
|
261
|
+
|
|
262
|
+
def visit_Keyword(self, node):
|
|
263
|
+
if not node.name:
|
|
264
|
+
return
|
|
265
|
+
kw_nid = _kw_id(node.name)
|
|
266
|
+
add_node(kw_nid, node.name, node.lineno)
|
|
267
|
+
add_edge(file_nid, kw_nid, "contains", node.lineno)
|
|
268
|
+
prev, self.scope_nid = self.scope_nid, kw_nid
|
|
269
|
+
self.generic_visit(node)
|
|
270
|
+
self.scope_nid = prev
|
|
271
|
+
|
|
272
|
+
# Calls (current test/keyword scope, file scope for suite fixtures)
|
|
273
|
+
def visit_KeywordCall(self, node):
|
|
274
|
+
if node.keyword:
|
|
275
|
+
add_call_edges(self.scope_nid, node.keyword, node.lineno)
|
|
276
|
+
self.generic_visit(node)
|
|
277
|
+
|
|
278
|
+
def visit_Setup(self, node):
|
|
279
|
+
if node.name:
|
|
280
|
+
add_call_edges(self.scope_nid, node.name, node.lineno)
|
|
281
|
+
|
|
282
|
+
def visit_Teardown(self, node):
|
|
283
|
+
if node.name:
|
|
284
|
+
add_call_edges(self.scope_nid, node.name, node.lineno)
|
|
285
|
+
|
|
286
|
+
def visit_Template(self, node):
|
|
287
|
+
# Template statements carry the keyword in .value, not .name
|
|
288
|
+
if node.value:
|
|
289
|
+
add_call_edges(self.scope_nid, node.value, node.lineno)
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
_RobotVisitor().visit(model)
|
|
293
|
+
except Exception as e:
|
|
294
|
+
return {"nodes": nodes, "edges": edges, "error": str(e)}
|
|
295
|
+
|
|
296
|
+
return {"nodes": nodes, "edges": edges}
|