Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/coverage/detail/lang.solidity.core.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Auto-generated. Back to [summary](../summary.md).
| Capability | Status | Verified at | Issue | Cites | Notes |
|------------|--------|-------------|-------|-------|-------|
| Call line precision | 🟢 `partial` | `2026-06-24` | 5371 | `internal/extractors/solidity/extractor.go`<br>`internal/extractors/solidity/extractor_test.go` | collectCallsFromBody emits CALLS edges per dotted (Type.method()) and bare (fn()) invocation in a function/modifier body, with a solidityKeywords denylist dropping control-flow/builtins/type/visibility tokens and self-recursion filtering on undotted leaves (dotted cross-contract calls preserved, #2114). Partial (honest): edges do NOT yet carry a per-call line property; targets are unresolved name strings, not bound to the callee SCOPE.Operation. |
| Core extraction | ✅ `full` | `2026-06-24` | 5371 | `internal/extractors/solidity/extractor.go`<br>`internal/extractors/solidity/extractor_test.go` | Regex extractor (no bundled tree-sitter Solidity grammar). contractRE emits contract/library/interface/abstract-contract as SCOPE.Component (subtype contract/library/interface); functionRE/eventRE/modifierRE emit members as SCOPE.Operation (subtype function/event/modifier) with CONTAINS edges from the owning contract. Comments and string literals are scrubbed (stripCommentsAndStrings) and brace-matched bodies extracted (extractBracedBody) before member/call scanning. Proven by the extractor_test.go suite (contract/library/interface discovery, member CONTAINS, signature capture). |
| Core extraction | ✅ `full` | `2026-07-28` | 5371 | `internal/extractors/solidity/extractor.go`<br>`internal/extractors/solidity/extractor_test.go` | Regex extractor (no bundled tree-sitter Solidity grammar). contractRE emits contract/library/interface/abstract-contract as SCOPE.Component (subtype contract/library/interface); functionRE/eventRE/modifierRE emit members as SCOPE.Operation (subtype function/event/modifier) with CONTAINS edges from the owning contract. Comments and string literals are scrubbed (stripCommentsAndStrings) and brace-matched bodies extracted (extractBracedBody) before member/call scanning. The member regexes end at the opening paren, so declSignature scans on from the match with paren depth to the terminating { or ; and Signature carries the full parameter list, mutability/visibility and returns clause; before that it stopped at the paren (5255 truncated signatures across seven production Solidity repos, none complete). Proven by the extractor_test.go suite (contract/library/interface discovery, member CONTAINS, signature capture). |
| Import resolution quality | 🟢 `partial` | `2026-06-24` | 5371 | `internal/extractors/solidity/extractor.go`<br>`internal/extractors/solidity/extractor_test.go` | buildImportEntities parses both import styles (import "./Foo.sol"; and import {Sym} from "pkg/...";), emitting one IMPORTS edge per target path with source_module/imported_name/local_name properties and a SCOPE.Component for the imported unit. Partial (honest): named-symbol imports record only the module path (not per-symbol bindings) and edges point at the raw path string, not a resolved file/contract entity. |

## Provenance
Expand Down
4 changes: 2 additions & 2 deletions docs/coverage/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -89133,8 +89133,8 @@
"status": "full",
"cites": ["internal/extractors/solidity/extractor.go","internal/extractors/solidity/extractor_test.go"],
"issue": "5371",
"notes": "Regex extractor (no bundled tree-sitter Solidity grammar). contractRE emits contract/library/interface/abstract-contract as SCOPE.Component (subtype contract/library/interface); functionRE/eventRE/modifierRE emit members as SCOPE.Operation (subtype function/event/modifier) with CONTAINS edges from the owning contract. Comments and string literals are scrubbed (stripCommentsAndStrings) and brace-matched bodies extracted (extractBracedBody) before member/call scanning. Proven by the extractor_test.go suite (contract/library/interface discovery, member CONTAINS, signature capture).",
"verified_at": "2026-06-24"
"notes": "Regex extractor (no bundled tree-sitter Solidity grammar). contractRE emits contract/library/interface/abstract-contract as SCOPE.Component (subtype contract/library/interface); functionRE/eventRE/modifierRE emit members as SCOPE.Operation (subtype function/event/modifier) with CONTAINS edges from the owning contract. Comments and string literals are scrubbed (stripCommentsAndStrings) and brace-matched bodies extracted (extractBracedBody) before member/call scanning. The member regexes end at the opening paren, so declSignature scans on from the match with paren depth to the terminating { or ; and Signature carries the full parameter list, mutability/visibility and returns clause; before that it stopped at the paren (5255 truncated signatures across seven production Solidity repos, none complete). Proven by the extractor_test.go suite (contract/library/interface discovery, member CONTAINS, signature capture).",
"verified_at": "2026-07-28"
},
"import_resolution_quality": {
"status": "partial",
Expand Down
29 changes: 25 additions & 4 deletions internal/extractors/solidity/extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,7 @@ func findContracts(src, filePath string, signals frameworkSignals) []types.Entit
if fnEndLine == 0 {
fnEndLine = fnStartLine
}
sigEnd := fm[1]
rawFnSig := strings.Join(strings.Fields(body[fm[0]:sigEnd]), " ")
rawFnSig := declSignature(body, fm[0], fm[1])

callRels := collectCallsFromBody(fnBody, qualName)
fnRec := types.EntityRecord{
Expand Down Expand Up @@ -321,7 +320,7 @@ func findContracts(src, filePath string, signals frameworkSignals) []types.Entit
evName := body[em[2]:em[3]]
qualName := name + "." + evName
evStartLine := bodyLineOffset + strings.Count(body[:em[0]], "\n")
rawEvSig := strings.Join(strings.Fields(body[em[0]:em[1]]), " ")
rawEvSig := declSignature(body, em[0], em[1])

evRec := types.EntityRecord{
Name: qualName,
Expand Down Expand Up @@ -350,7 +349,7 @@ func findContracts(src, filePath string, signals frameworkSignals) []types.Entit
modName := body[mm[2]:mm[3]]
qualName := name + "." + modName
modStartLine := bodyLineOffset + strings.Count(body[:mm[0]], "\n")
rawModSig := strings.Join(strings.Fields(body[mm[0]:mm[1]]), " ")
rawModSig := declSignature(body, mm[0], mm[1])
modBody, _ := extractBracedBody(body, mm[1])

callRels := collectCallsFromBody(modBody, qualName)
Expand Down Expand Up @@ -548,6 +547,28 @@ func extractBracedBody(src string, openPos int) (string, int) {
return "", 0
}

// declSignature returns the declaration starting at declStart up to the first
// '{' or ';' outside parentheses, with runs of whitespace collapsed. src is
// scrubbed (see stripCommentsAndStrings), so a delimiter inside a comment or a
// string literal cannot end the signature early. Falls back to the span up to
// fallbackEnd when the declaration has no terminator.
func declSignature(src string, declStart, fallbackEnd int) string {
depth := 0
for i := declStart; i < len(src); i++ {
switch src[i] {
case '(':
depth++
case ')':
depth--
case '{', ';':
if depth == 0 {
return strings.Join(strings.Fields(src[declStart:i]), " ")
}
}
}
return strings.Join(strings.Fields(src[declStart:fallbackEnd]), " ")
}

// stripCommentsAndStrings replaces Solidity // and /* */ comments and string
// literals with spaces so regexes don't match inside them.
func stripCommentsAndStrings(src string) string {
Expand Down
125 changes: 125 additions & 0 deletions internal/extractors/solidity/extractor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,131 @@ contract StakingVault {
}
}

// ── Signature extraction ──────────────────────────────────────────────────────

func TestSolidity_FunctionSignatures(t *testing.T) {
src := `// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IVault {
function totalSupply() external view returns (uint256);
}

contract Vault is IVault {
function totalSupply() external view returns (uint256) {
return _total;
}

function transfer(
address to,
uint256 amount
) public onlyOwner returns (bool ok) {
return true;
}

function forEach(function(uint256) external returns (bool) cb) public {
cb(1);
}
}
`
ents := runSolidity(t, src, "Vault.sol")

want := map[string]string{
"IVault.totalSupply": "function totalSupply() external view returns (uint256)",
"Vault.totalSupply": "function totalSupply() external view returns (uint256)",
"Vault.transfer": "function transfer( address to, uint256 amount ) public onlyOwner returns (bool ok)",
"Vault.forEach": "function forEach(function(uint256) external returns (bool) cb) public",
}
for name, sig := range want {
fn := solFindSubtype(ents, name, "SCOPE.Operation", "function")
if fn == nil {
t.Errorf("expected function %s", name)
continue
}
if fn.Signature != sig {
t.Errorf("%s signature = %q, want %q", name, fn.Signature, sig)
}
}
}

func TestSolidity_EventAndModifierSignatures(t *testing.T) {
src := `// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Vault {
event Transfer(address indexed from, address indexed to, uint256 value);

modifier onlyRoleOrOwner(bytes32 role) {
require(hasRole(role, msg.sender) || msg.sender == owner, "denied");
_;
}
}
`
ents := runSolidity(t, src, "Vault.sol")

ev := solFindSubtype(ents, "Vault.Transfer", "SCOPE.Operation", "event")
if ev == nil {
t.Fatal("expected event Vault.Transfer")
}
const wantEv = "event Transfer(address indexed from, address indexed to, uint256 value)"
if ev.Signature != wantEv {
t.Errorf("Transfer signature = %q, want %q", ev.Signature, wantEv)
}

mod := solFindSubtype(ents, "Vault.onlyRoleOrOwner", "SCOPE.Operation", "modifier")
if mod == nil {
t.Fatal("expected modifier Vault.onlyRoleOrOwner")
}
const wantMod = "modifier onlyRoleOrOwner(bytes32 role)"
if mod.Signature != wantMod {
t.Errorf("onlyRoleOrOwner signature = %q, want %q", mod.Signature, wantMod)
}
}

func TestSolidity_SignatureIgnoresDelimitersInCommentsAndStrings(t *testing.T) {
src := `// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Guarded {
function withdraw(
address, // recipient )
uint256 amount
) public onlyRole("ADMIN)ROLE") {
_send(amount);
}

function upgrade(
uint64, /* newVersion ) */
bytes calldata /* data */
) internal virtual override {
_bump();
}

event Logged(uint256 a /* ) */, uint256 b);
}
`
ents := runSolidity(t, src, "Guarded.sol")

// Blanking leaves the erased comment/string body visible as a gap.
cases := []struct {
name, subtype, want string
}{
{"Guarded.withdraw", "function", "function withdraw( address, uint256 amount ) public onlyRole( )"},
{"Guarded.upgrade", "function", "function upgrade( uint64, bytes calldata ) internal virtual override"},
{"Guarded.Logged", "event", "event Logged(uint256 a , uint256 b)"},
}
for _, tc := range cases {
e := solFindSubtype(ents, tc.name, "SCOPE.Operation", tc.subtype)
if e == nil {
t.Errorf("expected %s %s", tc.subtype, tc.name)
continue
}
if e.Signature != tc.want {
t.Errorf("%s signature = %q, want %q", tc.name, e.Signature, tc.want)
}
}
}

// ── CONTAINS edges ────────────────────────────────────────────────────────────

func TestSolidity_ContainsEdges(t *testing.T) {
Expand Down
17 changes: 17 additions & 0 deletions tools/coverage/capability-map.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14165,6 +14165,23 @@ records:
issues_implemented: ["2773"]
verified_at: "2026-05-28"

lang.solidity.core:
capabilities:
core_extraction:
# Regex extractor: no tree-sitter Solidity grammar is bundled.
# stripCommentsAndStrings blanks comments and literals first;
# findContracts walks the declarations; declSignature scans past the
# member regex match with paren depth so Signature is complete.
status: full
symbols:
- file: internal/extractors/solidity/extractor.go
functions: [findContracts, declSignature, extractBracedBody, stripCommentsAndStrings]
tests:
- file: internal/extractors/solidity/extractor_test.go
functions: [TestSolidity_ContractDeclaration, TestSolidity_LibraryDeclaration, TestSolidity_InterfaceDeclaration, TestSolidity_Functions, TestSolidity_Events, TestSolidity_Modifiers, TestSolidity_ContainsEdges, TestSolidity_FunctionSignatures, TestSolidity_EventAndModifierSignatures, TestSolidity_SignatureIgnoresDelimitersInCommentsAndStrings]
issues_implemented: [6028]
verified_at: "2026-07-28"

lang.c-cpp.framework.crow:
capabilities:
Type System:
Expand Down
Loading