Skip to content

feat: add Solidity language extractor#1874

Open
babyblueviper1 wants to merge 1 commit into
Graphify-Labs:v8from
babyblueviper1:feat/solidity-extractor
Open

feat: add Solidity language extractor#1874
babyblueviper1 wants to merge 1 commit into
Graphify-Labs:v8from
babyblueviper1:feat/solidity-extractor

Conversation

@babyblueviper1

Copy link
Copy Markdown

Summary

Adds a Solidity extractor (graphify/extractors/solidity.py) over tree-sitter-solidity, following the existing per-language extractor pattern in this package (closest analog: rust.py).

Extracts: contracts, interfaces, libraries, functions/constructors/modifiers, structs, enums, events, inheritance (is A, B), imports (named {A, B as C}, namespace * as X, and bare), and same-file call resolution — including modifier invocations, which are a Solidity-specific call site with no explicit call syntax.

Scope (disclosed honestly, not oversold)

Call resolution here is by same-file method-name matching, not receiver-type checking — matches rust.py's label_to_nid tradeoff exactly. This gets the common cases right (a storage variable typed as a locally-defined interface, Library.staticMethod(), InterfaceName(addr) cast-calls, modifier invocations) but:

  • can mismatch if two unrelated types in the same file happen to share a method name
  • never resolves a call to something this file doesn't itself define (an imported library/interface whose body lives elsewhere) — those are emitted via raw_calls for a possible future cross-file pass, same as several other extractors already do

Full type-qualified receiver resolution (the kind the larger OOP-language blocks in extract.py do for Java/C#/TS) is out of scope for this first cut — happy to follow up if that's wanted.

Testing

  • 14 new tests in tests/test_multilang.py + tests/fixtures/sample.sol, covering nodes, inherits, method/contains, import context, same-file call resolution, modifier-invocation calls, confidence, dangling-edge safety, and the missing-file error path. All pass.
  • pytest tests/ -q on the full suite: no new failures introduced (confirmed by running the same set of failing tests against the clean v8 HEAD before my change — same ~24 pre-existing failures on unrelated modules with missing optional deps/fixtures, e.g. tree-sitter-hcl not installed, test_skillgen.py audit baselines, one pre-existing test-ordering flake in test_cache.py unrelated to any language extractor).
  • Ran the extractor against 16 real, non-vendored .sol files from a live project (contracts + Foundry tests) — zero extraction errors, and a real internal helper-function call chain (validationRequest() → _authorized() → _agentOwner()) resolved correctly end to end.
  • ruff check --select E9,F63,F7,F82 clean on the new/touched files.

Checklist (per ARCHITECTURE.md § Adding a new language extractor)

  • extract_solidity(path: Path) -> dict following the existing pattern
  • Registered .sol in extract.py's _DISPATCH
  • Added .sol to CODE_EXTENSIONS in detect.py (covers watch.py's _WATCHED_EXTENSIONS, which derives from it — no separate edit needed there)
  • Added tree-sitter-solidity>=1.0,<2.0 to pyproject.toml
  • Fixture + tests in tests/test_multilang.py

Structural extraction over tree-sitter-solidity: contracts, interfaces,
libraries, functions/constructors/modifiers, structs, enums, and events,
plus inheritance (`is A, B`), imports (named/namespace/bare), and same-file
call resolution -- including modifier invocations, which are Solidity-
specific call sites with no explicit call syntax.

Follows the existing extractor pattern (rust.py/verilog.py): add_node/
add_edge helpers, the sourceless-stub convention for cross-file references
(ensure_named_node), and the label_to_nid same-file call resolution rust.py
uses, extended to Solidity's `X.method()` / bare-call / modifier-invocation
call sites.

Scope, disclosed in the module docstring: call resolution is by same-file
method-NAME matching, not receiver-type checking, so it can occasionally
mismatch if two unrelated types in the same file share a method name (same
tradeoff rust.py's label_to_nid already makes) and never resolves a call to
something this file doesn't itself define (raw_calls carries those for a
possible future cross-file pass). Full type-qualified receiver resolution,
the kind the larger OOP-language blocks in extract.py do, is out of scope
for this first cut.

Tested against 16 real, non-vendored .sol files from a live project
(contracts + Foundry tests) with zero extraction errors, correct
intra-contract call graphs (verified a real internal helper-function call
chain resolves correctly), and correct inheritance/import edges.

- graphify/extractors/solidity.py: extract_solidity()
- graphify/extract.py: import + `.sol` dispatch entry
- graphify/detect.py: `.sol` added to CODE_EXTENSIONS (also covers
  watch.py's _WATCHED_EXTENSIONS, which derives from it)
- pyproject.toml: tree-sitter-solidity>=1.0,<2.0 dependency
- tests/fixtures/sample.sol + 14 new tests in tests/test_multilang.py
  covering nodes, inherits, method/contains, imports, same-file calls,
  modifier-invocation calls, confidence, dangling-edge safety, and the
  missing-file error path
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.

1 participant