Skip to content

feat: Robot Framework extractor for .robot/.resource files (#3192) - #3211

Closed
nshiveg wants to merge 4 commits into
Graphify-Labs:v8from
nshiveg:feat/robot-framework-extractor-3192
Closed

feat: Robot Framework extractor for .robot/.resource files (#3192)#3211
nshiveg wants to merge 4 commits into
Graphify-Labs:v8from
nshiveg:feat/robot-framework-extractor-3192

Conversation

@nshiveg

@nshiveg nshiveg commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

feat: Robot Framework extractor for .robot/.resource files

Closes #3192

Problem

Robot Framework files are invisible to graphify: .robot/.resource are in neither detect.CODE_EXTENSIONS nor _DISPATCH, so on Robot-based automation repos the graph maps the Python keyword libraries but none of the suites, test cases, user keywords, or the resource-import topology that actually structures such a repo (details in #3192).

Approach

No maintained tree-sitter grammar exists for Robot Framework, so graphify/extractors/robot.py uses the official parser (robot.api.get_model / get_resource_model + ModelVisitor), as proposed in the issue. Pure Python, wheels everywhere, added as an optional robot extra following the terraform/ocaml pattern, and wired into _EXTRA_FOR_EXTENSION so the #1745 warning names the extra when the parser is missing.

Nodes: the suite/resource file, its test cases, its user keywords, plus stub nodes for third-party named libraries (SeleniumLibrary, RequestsLibrary, ...) so suites sharing a library cluster together (same pattern as the Svelte bare-import stubs). RF standard libraries (BuiltIn, Collections, ...) are filtered like the built-ins in _LANGUAGE_BUILTIN_GLOBALS (#726) — they would otherwise become instant god nodes.

Edges: contains (file → test/keyword), imports (Resource / path-form Library / Variables imports resolved onto the imported file's own node id, preserving the relative/absolute path form like the JS relative-import resolver; ${CURDIR}/${/}/${EXECDIR} are substituted, and an import whose path still holds an unresolvable ${VARIABLE} emits nothing), and calls (test/keyword/suite-fixture → keyword, including [Setup]/[Teardown]/[Template]/Test Template and calls nested in FOR/IF blocks).

Design note — keyword ids are bare names, not stem-qualified: Robot resolves keywords globally by name across imported resources, so bare ids make a suite's call edge land on the defining resource's keyword node with no separate cross-file resolution pass. Test-case nodes stay stem-qualified because test names repeat across suites. Calls to keywords never defined in the corpus (e.g. BuiltIn's Log) dangle and are dropped by build_from_json like any external reference.

One parser subtlety encoded in the tests: Template/Test Template statements carry their keyword in .value (not .name like Setup/Teardown) — missing this silently drops all template edges.

Tests

5 tests in tests/test_languages.py (skipif-guarded like the dm/commonlisp extras) + 2 fixtures (sample.robot, robot_keywords.resource) covering: node extraction, call edges incl. fixtures/loops/templates, import resolution + stdlib filtering + unresolvable-variable paths, .resource parsing, and the cross-file id guarantee (suite call-edge target == resource definition id). ruff and python -m tools.skillgen --check pass.

Real-world validation

Run against an industrial Robot Framework automation repo (~850 code files, ~5,000 tests/keywords): the graph grew from 5,104 to 15,768 nodes — 10,577 robot-sourced — with 44,423 keyword-call edges and 440 robot→python Library edges resolving onto the existing Python nodes. God nodes shifted from generic Python helpers to the actual shared simulator-control keywords, which matches how that codebase is really wired.

🤖 Generated with Claude Code

…Labs#3192)

- graphify/extractors/robot.py: official robot.api parser (no maintained
  tree-sitter grammar exists for Robot Framework); extracts suites, test
  cases, user keywords, Resource/Library/Variables imports, and keyword-call
  edges (incl. suite/test fixtures, [Template]/Test Template, and calls
  nested in FOR/IF blocks). Keyword nodes use bare-name ids so cross-file
  calls land on the defining resource without a resolution pass; RF stdlib
  imports are filtered like _LANGUAGE_BUILTIN_GLOBALS (Graphify-Labs#726); third-party
  libraries get stub nodes like the svelte bare-import stubs.
- detect.py/_DISPATCH wiring, optional [robot] extra (terraform/ocaml
  pattern), _EXTRA_FOR_EXTENSION so the Graphify-Labs#1745 warning names the extra.
- 5 tests + 2 fixtures following the test_languages.py conventions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 4 advisory finding(s) below merit a look before merge.


Graphify review — findings

Adds a Robot Framework extractor for .robot/.resource files, wired through the robot optional extra. extract_robot parses via the official robot.api model and emits nodes for the suite/resource file, its test cases, and its user keywords, plus contains, imports, and calls edges — resolving Resource/Variables/path-form Library imports onto the imported file's own node while non-stdlib named libraries (e.g. SeleniumLibrary) get stub nodes so suites sharing a library cluster; standard libraries are treated as noise and skipped. Keyword nodes are keyed by bare name so cross-file call edges land on the defining node without a resolution pass, test cases stay stem-qualified, and calls to keywords absent from the corpus drop out during graph build; when robotframework isn't installed the extractor returns an install-hint error instead of failing.

Worth a look

  • Optional robot extra is imported unconditionallygraphify/extract.py:55 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Keyword IDs collide across files by bare name, merging distinct keywordsgraphify/extractors/robot.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • ${CURDIR} imports from relative source paths are prefixed twicegraphify/extractors/robot.py:28 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Robot keyword calls are not normalized like Robot Framework matchinggraphify/extractors/robot.py:118 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2618 functions depend on the 911 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 517 callers, 43 callees
  • new: _rebuild_code() — 108 callers, 50 callees
  • new: detect() — 108 callers, 15 callees
  • new: save_manifest() — 40 callers, 11 callees
  • new: extract_files_direct() — 17 callers, 20 callees
  • new: extract_js() — 85 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: extract_corpus_parallel() — 26 callers, 11 callees
  • …and 41 more — each is listed as a finding

Verification — 2618 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 2380 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below; 48 more finding(s) on lines outside this diff (see the check run).

return p


def extract_robot(path: Path) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_robot()

6 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

- ${CURDIR}/${EXECDIR} imports no longer double the source-dir prefix for
  relative scan paths, and resolved paths are normpathed unconditionally so
  ../ segments collapse (ids now always match the imported file's node).
- Keyword node ids now normalize the way Robot Framework matches keywords
  (case-, space-, and underscore-insensitive) via _kw_id, so OpenSession /
  open_session / Open Session share one node.
- Regression tests for both, plus a test proving graphify.extract imports
  and degrades gracefully when robotframework is absent (the robot.api
  import is lazy - answers the unconditional-import finding).
- Same-name keyword merging across files documented as deliberate (mirrors
  Robot's single global keyword namespace).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nshiveg

nshiveg commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the automated review — findings addressed in 58e19cc:

  • ${CURDIR} double-prefix — confirmed and fixed. ${CURDIR}/${EXECDIR} now mark the path as anchored so it is no longer re-joined with the source dir (which doubled the prefix for relative scan paths), and resolved paths are normpathed unconditionally so ../ segments collapse for absolute sources too. Regression test: test_robot_curdir_and_execdir_imports_resolve_without_double_prefix.
  • Keyword-call normalization — fixed. Keyword node ids now normalize exactly the way Robot Framework matches keyword names (case-, space-, and underscore-insensitive) via _kw_id, so OpenSession / open_session / Open Session share one node. Test: test_robot_keyword_matching_is_case_space_underscore_insensitive.
  • "Optional robot extra is imported unconditionally" — not the case: graphify/extractors/robot.py imports robot.api lazily inside extract_robot(), so importing graphify.extract works without robotframework and the extractor returns the install-hint error dict (same pattern as the terraform extractor's tree_sitter_hcl handling). Now codified as a test that blocks the robot package at import level in a subprocess: test_robot_extractor_degrades_gracefully_without_robotframework.
  • Bare-name keyword id merging across files — deliberate. Robot resolves keywords in a single global namespace, so two files defining the same keyword name are one name to Robot (and ambiguous to call); merging them is what makes cross-file call edges land on definitions with no separate resolution pass. Now called out explicitly in the extractor docstring.

All 8 robot tests pass; ruff and tools.skillgen --check clean.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.


Graphify review — findings

Adds a Robot Framework extractor (extract_robot) for .robot/.resource files, wiring .robot/.resource into CODE_EXTENSIONS, the extractor dispatch, and the robot optional extra so files without robotframework installed return a clear install-hint error instead of parsing. It emits suite/test-case/keyword nodes plus contains, imports, and calls edges via the official robot.api parser: keywords key on a bare name normalized case/space/underscore-insensitively (_kw_id) so cross-file calls land on the defining node and same-named keywords merge like Robot's global namespace, while resource/library imports resolve through _resolve_robot_import (handling ${CURDIR}/${EXECDIR} anchoring and bailing to None on unresolved variables) and standard libraries are skipped as noise.

Worth a look

  • ${EXECDIR} imports become relative even when source paths are absolutegraphify/extractors/robot.py:34 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • BDD-style Robot calls do not resolve to their keyword definitionsgraphify/extractors/robot.py:130 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2623 functions depend on the 916 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 517 callers, 43 callees
  • new: _rebuild_code() — 108 callers, 50 callees
  • new: detect() — 108 callers, 15 callees
  • new: save_manifest() — 40 callers, 11 callees
  • new: extract_files_direct() — 17 callers, 20 callees
  • new: extract_js() — 85 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: extract_corpus_parallel() — 26 callers, 11 callees
  • …and 41 more — each is listed as a finding

Verification — 2623 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 2385 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below; 48 more finding(s) on lines outside this diff (see the check run).

return _make_id(name.replace(" ", "").replace("_", ""))


def extract_robot(path: Path) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_robot()

7 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

- ${EXECDIR} is the directory Robot was launched from - statically knowable
  only for a relative scan (where '.' aligns with the scan root). For an
  absolute source path the resolved id could never match a node, so the
  import now emits nothing instead of a guaranteed-dangling relative edge.
- BDD-style calls (Given/When/Then/And/But <keyword>) now emit call edges
  for both the full and the prefix-stripped candidate, mirroring Robot's
  try-full-then-stripped resolution; the nonexistent candidate dangles and
  is dropped by the graph builder.
- Regression tests for both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nshiveg

nshiveg commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Both round-2 findings addressed in 8181676:

  • ${EXECDIR} in absolute-path scans — confirmed and fixed. ${EXECDIR} (the directory Robot was launched from) is statically knowable only for a relative scan, where . aligns with the scan root and matches how relative file-node ids are built. For an absolute source path the resolved id could never match a node, so the import now emits nothing instead of a guaranteed-dangling relative edge. Test extended: test_robot_curdir_and_execdir_imports_resolve_without_double_prefix.
  • BDD-style calls — confirmed and fixed. Given/When/Then/And/But <keyword> calls now emit call edges for both the full and the one-prefix-stripped candidate, mirroring Robot's try-full-then-stripped resolution; whichever keyword exists receives the edge and the other candidate dangles and is dropped by the graph builder (no fabricated nodes). English prefixes only — localized BDD prefixes are out of scope. New test: test_robot_bdd_style_calls_reach_their_definitions, including a keyword literally named with a prefix.

9/9 robot tests pass; ruff clean.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 3 advisory finding(s) below merit a look before merge.


Graphify review — findings

Adds a Robot Framework extractor for .robot/.resource files behind a new robot optional dependency, wiring .robot/.resource into CODE_EXTENSIONS, the _DISPATCH map, and _EXTRA_FOR_EXTENSION so missing robotframework yields an install-hint error rather than a crash. extract_robot parses via the official robot.api model to emit suite/test-case/user-keyword nodes plus contains, imports, and calls edges, resolving Resource/Library/Variables imports onto the imported file's own node ID (skipping standard libraries and unresolvable ${VAR} paths) and keying keywords by name normalized case/space/underscore-insensitively so cross-file calls land on the defining node. BDD-prefixed calls (Given/When/Then/And/But) resolve by trying the full name then the stripped name, and ${EXECDIR}-anchored imports emit nothing under an absolute scan since the ID could never match.

Worth a look

  • Built-in Robot path variables are matched case-sensitivelygraphify/extractors/robot.py:28 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • _resolve_robot_import double-prefixes when EXECDIR path also needs parent join is prevented but CURDIR replacement may produce non-anchored absolutegraphify/extractors/robot.py:47 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Robot keyword IDs are not case-normalizedgraphify/extractors/robot.py:63 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2625 functions depend on the 918 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 517 callers, 43 callees
  • new: _rebuild_code() — 108 callers, 50 callees
  • new: detect() — 108 callers, 15 callees
  • new: save_manifest() — 40 callers, 11 callees
  • new: extract_files_direct() — 17 callers, 20 callees
  • new: extract_js() — 85 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: extract_corpus_parallel() — 26 callers, 11 callees
  • …and 41 more — each is listed as a finding

Verification — 2625 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 2387 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below; 48 more finding(s) on lines outside this diff (see the check run).

return _make_id(name.replace(" ", "").replace("_", ""))


def extract_robot(path: Path) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_robot()

8 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Robot matches variable names case-, space-, and underscore-insensitively,
so ${curdir} / ${Cur_Dir} / ${EXEC DIR} now resolve like their canonical
forms in Settings-section imports; any other variable, in any casing,
still yields no edge. Regression test:
test_robot_path_variables_match_case_space_underscore_insensitively.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nshiveg

nshiveg commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Round-3 findings, with evidence:

  • Built-in Robot path variables matched case-sensitively - fair nit, fixed in 25dca92: variable names in Settings-section imports now match the way Robot matches them (case-, space-, and underscore-insensitive), so ${curdir} / ${Cur_Dir} / ${EXEC DIR} resolve like ${CURDIR}; any other variable, in any casing, still yields no edge. Test: test_robot_path_variables_match_case_space_underscore_insensitively.
  • "CURDIR replacement may produce non-anchored absolute" - no reproducible failure mode found: ${CURDIR} with a relative source resolves without double-prefixing, and with an absolute source comes back normpathed (no leftover ..), both asserted by test_robot_curdir_and_execdir_imports_resolve_without_double_prefix. Happy to fix a concrete counter-example if one exists.
  • "Robot keyword IDs are not case-normalized" - already the case since 58e19cc: _kw_id lowercases (via make_id) after stripping spaces/underscores, and test_robot_keyword_matching_is_case_space_underscore_insensitive asserts OPEN SESSION / open_session / OpenSession share one node. This re-raises a finding fixed in the previous commit - possibly analyzed against a stale hunk.

10/10 robot tests pass; ruff clean.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.


Graphify review — findings

Adds a Robot Framework extractor for .robot/.resource files, driven by the official robot.api parser and gated behind the new robot install extra (returning an error node when robotframework isn't installed). extract_robot emits nodes for the suite/resource file, its test cases, and user keywords, plus contains, imports, and calls edges; keyword nodes are keyed by a name normalized case-, space-, and underscore-insensitively via _kw_id so cross-file calls land on the defining node under Robot's single global namespace, while test cases stay stem-qualified. _resolve_robot_import resolves Settings-section imports onto the imported file's own node ID, handling ${CURDIR}/${EXECDIR} anchoring and skipping unresolvable variables (including ${EXECDIR} under absolute scans, where no matching node could exist), and standard libraries in _ROBOT_STDLIBS are treated as noise so only third-party libraries get stub nodes.

Worth a look

  • Optional Robot extractor is imported at module loadgraphify/extract.py:55 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2626 functions depend on the 919 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 517 callers, 43 callees
  • new: _rebuild_code() — 108 callers, 50 callees
  • new: detect() — 108 callers, 15 callees
  • new: save_manifest() — 40 callers, 11 callees
  • new: extract_files_direct() — 17 callers, 20 callees
  • new: extract_js() — 85 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: extract_corpus_parallel() — 26 callers, 11 callees
  • …and 41 more — each is listed as a finding

Verification — 2626 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 2388 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below; 48 more finding(s) on lines outside this diff (see the check run).

return _make_id(name.replace(" ", "").replace("_", ""))


def extract_robot(path: Path) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_robot()

8 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@safishamsi

Copy link
Copy Markdown
Collaborator

Shipped in v0.9.53 via authorship-preserving cherry-pick so you keep contributor-graph credit. Thanks @nshiveg! Release: https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.53

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Add Robot Framework .robot and .resource parsing support

2 participants