diff --git a/Cargo.lock b/Cargo.lock index 9078d52..1b513b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -432,7 +432,7 @@ dependencies = [ [[package]] name = "codegraph-bench" -version = "0.33.0" +version = "0.34.0" dependencies = [ "anyhow", "clap", @@ -447,7 +447,7 @@ dependencies = [ [[package]] name = "codegraph-core" -version = "0.33.0" +version = "0.34.0" dependencies = [ "anyhow", "serde", @@ -463,7 +463,7 @@ dependencies = [ [[package]] name = "codegraph-daemon" -version = "0.33.0" +version = "0.34.0" dependencies = [ "anyhow", "codegraph-core", @@ -483,7 +483,7 @@ dependencies = [ [[package]] name = "codegraph-extract" -version = "0.33.0" +version = "0.34.0" dependencies = [ "anyhow", "codegraph-core", @@ -518,6 +518,7 @@ dependencies = [ "tree-sitter-ruby", "tree-sitter-rust", "tree-sitter-scala", + "tree-sitter-solidity", "tree-sitter-swift", "tree-sitter-typescript", "tree-sitter-xml", @@ -526,7 +527,7 @@ dependencies = [ [[package]] name = "codegraph-graph" -version = "0.33.0" +version = "0.34.0" dependencies = [ "anyhow", "codegraph-core", @@ -540,7 +541,7 @@ dependencies = [ [[package]] name = "codegraph-mcp" -version = "0.33.0" +version = "0.34.0" dependencies = [ "anyhow", "axum", @@ -563,7 +564,7 @@ dependencies = [ [[package]] name = "codegraph-resolve" -version = "0.33.0" +version = "0.34.0" dependencies = [ "anyhow", "codegraph-core", @@ -577,7 +578,7 @@ dependencies = [ [[package]] name = "codegraph-rs" -version = "0.33.0" +version = "0.34.0" dependencies = [ "anyhow", "assert_cmd", @@ -613,7 +614,7 @@ dependencies = [ [[package]] name = "codegraph-store" -version = "0.33.0" +version = "0.34.0" dependencies = [ "anyhow", "codegraph-core", @@ -625,7 +626,7 @@ dependencies = [ [[package]] name = "codegraph-watch" -version = "0.33.0" +version = "0.34.0" dependencies = [ "anyhow", "codegraph-core", @@ -3668,6 +3669,16 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "tree-sitter-solidity" +version = "1.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eacf8875b70879f0cb670c60b233ad0b68752d9e1474e6c3ef168eea8a90b25" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-swift" version = "0.7.3" diff --git a/README.md b/README.md index 2d08937..170b1be 100644 --- a/README.md +++ b/README.md @@ -439,7 +439,7 @@ Omit `--install` to print to stdout. Full per-shell install paths and notes: ## What CodeGraph Does (and Doesn't) -**Does:** deterministic code-structure extraction across 33 languages (TypeScript, +**Does:** deterministic code-structure extraction across 34 languages (TypeScript, Python, Go, Rust, Java, C/C++, C#, Vue, Svelte, GDScript, and more — see [`docs/languages.md`](docs/languages.md)), cross-file resolution (including Godot project graphs), graph traversal, FTS5 search, whole-graph export with @@ -454,9 +454,9 @@ beyond the fixed `LANGUAGES` set. ## Supported Languages -CodeGraph supports **33 languages** grouped by extraction depth. Quick overview: +CodeGraph supports **34 languages** grouped by extraction depth. Quick overview: -- **Tier 1 — Full symbol extraction (24):** TypeScript, TSX, JavaScript, JSX, ArkTS, Python, Go, Rust, Java, C, C++, C#, PHP, Ruby, Swift, Kotlin, Dart, Scala, Lua, Luau, Objective-C, R, GDScript, Pascal. +- **Tier 1 — Full symbol extraction (25):** TypeScript, TSX, JavaScript, JSX, ArkTS, Python, Go, Rust, Java, C, C++, C#, PHP, Ruby, Swift, Kotlin, Dart, Scala, Lua, Luau, Objective-C, R, Solidity, GDScript, Pascal. - **Tier 2 — Embedded / template extraction (6):** Vue, Svelte, Astro, Razor/`.cshtml`, Liquid, XML/MyBatis mapper. - **Tier 3 — File-level only (3):** YAML, Twig, Properties. diff --git a/crates/codegraph-bench/fixtures/solidity/IERC20.sol b/crates/codegraph-bench/fixtures/solidity/IERC20.sol new file mode 100644 index 0000000..2d40b91 --- /dev/null +++ b/crates/codegraph-bench/fixtures/solidity/IERC20.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IERC20 { + function transfer(address to, uint256 value) external returns (bool); +} diff --git a/crates/codegraph-bench/fixtures/solidity/Token.sol b/crates/codegraph-bench/fixtures/solidity/Token.sol new file mode 100644 index 0000000..ceb3788 --- /dev/null +++ b/crates/codegraph-bench/fixtures/solidity/Token.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./IERC20.sol"; + +error Unauthorized(address caller); + +uint256 constant MAX_SUPPLY = 1000000; + +contract Token is IERC20 { + uint256 public total; + + event Transfer(address indexed from, uint256 value); + + enum Status { + Active, + Closed + } + + struct Holder { + address account; + uint256 balance; + } + + modifier onlyOwner() { + _; + } + + constructor() { + total = MAX_SUPPLY; + } + + fallback() external { + } + + receive() external payable { + } + + function transfer(address to, uint256 value) public onlyOwner returns (bool) { + emit Transfer(to, value); + return true; + } +} + +library Math { + function add(uint256 a, uint256 b) internal pure returns (uint256) { + return a + b; + } +} diff --git a/crates/codegraph-bench/tests/equivalence.rs b/crates/codegraph-bench/tests/equivalence.rs index 3bee486..af8b299 100644 --- a/crates/codegraph-bench/tests/equivalence.rs +++ b/crates/codegraph-bench/tests/equivalence.rs @@ -135,6 +135,27 @@ fn arkts_db_is_self_equivalent_to_arkts_golden() { assert_equivalent(&arkts_db(), &arkts_golden_dir()).unwrap(); } +#[test] +fn generated_golden_matches_committed_solidity_fixture() { + // Guards Solidity extraction (upstream #1170): the `.sol`->Solidity mapping, + // contract/library->Class + interface->Interface + struct->Struct + + // enum->Enum, synthetic constructor/fallback/receive method names, + // state-var/struct-member/event/error->Field, `is`-inheritance->Extends + // (resolver promotes to Implements), and emit/modifier-guard call edges. + let tempdir = TestDir::new("generated-golden-solidity"); + write_golden(&solidity_db(), tempdir.path()).unwrap(); + + let expected = load_golden(&solidity_golden_dir()).unwrap(); + let actual = load_golden(tempdir.path()).unwrap(); + + diff_canonical(&expected, &actual, None).unwrap(); +} + +#[test] +fn solidity_db_is_self_equivalent_to_solidity_golden() { + assert_equivalent(&solidity_db(), &solidity_golden_dir()).unwrap(); +} + #[test] fn tier1_node_drift_is_reported() { let expected = load_golden(&mini_golden_dir()).unwrap(); @@ -237,6 +258,14 @@ fn arkts_golden_dir() -> PathBuf { workspace_root().join("reference/golden/arkts") } +fn solidity_db() -> PathBuf { + workspace_root().join("reference/golden/solidity/colby.db") +} + +fn solidity_golden_dir() -> PathBuf { + workspace_root().join("reference/golden/solidity") +} + struct TestDir { path: PathBuf, } diff --git a/crates/codegraph-core/src/types.rs b/crates/codegraph-core/src/types.rs index cb266e8..3e8985f 100644 --- a/crates/codegraph-core/src/types.rs +++ b/crates/codegraph-core/src/types.rs @@ -49,7 +49,7 @@ pub const EDGE_KIND_STRINGS: [&str; 12] = [ "decorates", ]; -pub const LANGUAGE_STRINGS: [&str; 37] = [ +pub const LANGUAGE_STRINGS: [&str; 38] = [ "typescript", "javascript", "tsx", @@ -78,6 +78,7 @@ pub const LANGUAGE_STRINGS: [&str; 37] = [ "luau", "objc", "r", + "solidity", "yaml", "twig", "xml", @@ -323,6 +324,8 @@ pub enum Language { ObjC, #[serde(rename = "r")] R, + #[serde(rename = "solidity")] + Solidity, #[serde(rename = "yaml")] Yaml, #[serde(rename = "twig")] @@ -344,7 +347,7 @@ pub enum Language { } impl Language { - pub const ALL: [Self; 37] = [ + pub const ALL: [Self; 38] = [ Self::TypeScript, Self::JavaScript, Self::Tsx, @@ -373,6 +376,7 @@ impl Language { Self::Luau, Self::ObjC, Self::R, + Self::Solidity, Self::Yaml, Self::Twig, Self::Xml, @@ -414,6 +418,7 @@ impl Language { Self::Luau => "luau", Self::ObjC => "objc", Self::R => "r", + Self::Solidity => "solidity", Self::Yaml => "yaml", Self::Twig => "twig", Self::Xml => "xml", diff --git a/crates/codegraph-extract/Cargo.toml b/crates/codegraph-extract/Cargo.toml index 2d3b46d..f1e183c 100644 --- a/crates/codegraph-extract/Cargo.toml +++ b/crates/codegraph-extract/Cargo.toml @@ -47,6 +47,7 @@ tree-sitter-r = "1.2.0" tree-sitter-ruby = "0.23.1" tree-sitter-rust = "0.24.2" tree-sitter-scala = "0.26.0" +tree-sitter-solidity = "1.2.13" tree-sitter-swift = "0.7.3" tree-sitter-typescript = "0.23.2" tree-sitter-xml = "0.7.0" diff --git a/crates/codegraph-extract/src/engine.rs b/crates/codegraph-extract/src/engine.rs index 054b157..3131c4d 100644 --- a/crates/codegraph-extract/src/engine.rs +++ b/crates/codegraph-extract/src/engine.rs @@ -82,6 +82,7 @@ pub fn builtin_language_for_ext(ext: &str) -> Option { "luau" => Language::Luau, "m" | "mm" => Language::ObjC, "r" => Language::R, + "sol" => Language::Solidity, "yml" | "yaml" => Language::Yaml, "twig" => Language::Twig, "xml" => Language::Xml, @@ -870,7 +871,7 @@ mod tests { assert_eq!(detect_language("s.metal"), Language::Cpp); assert_eq!(detect_language("k.cu"), Language::Cpp); assert_eq!(detect_language("k.cuh"), Language::Cpp); - assert_eq!(Language::ALL.len(), 37); + assert_eq!(Language::ALL.len(), 38); } #[test] @@ -878,6 +879,11 @@ mod tests { assert_eq!(detect_language("view.ets"), Language::ArkTs); } + #[test] + fn sol_maps_to_solidity() { + assert_eq!(detect_language("Token.sol"), Language::Solidity); + } + #[test] fn plain_ts_stays_typescript() { assert_eq!(detect_language("m.ts"), Language::TypeScript); diff --git a/crates/codegraph-extract/src/lang/mod.rs b/crates/codegraph-extract/src/lang/mod.rs index 8f8396c..14733d9 100644 --- a/crates/codegraph-extract/src/lang/mod.rs +++ b/crates/codegraph-extract/src/lang/mod.rs @@ -24,6 +24,7 @@ mod r; mod ruby; mod rust; mod scala; +mod solidity; mod swift; mod tsx; mod typescript; @@ -54,6 +55,7 @@ pub use r::R_SPEC; pub use ruby::RUBY_SPEC; pub use rust::RUST_SPEC; pub use scala::SCALA_SPEC; +pub use solidity::SOLIDITY_SPEC; pub use swift::SWIFT_SPEC; pub use tsx::TSX_SPEC; pub use typescript::TYPESCRIPT_SPEC; @@ -83,6 +85,7 @@ pub fn spec_for_language(language: Language) -> Option<&'static dyn LanguageSpec Language::Pascal => Some(&PASCAL_SPEC), Language::ObjC => Some(&OBJC_SPEC), Language::R => Some(&R_SPEC), + Language::Solidity => Some(&SOLIDITY_SPEC), Language::Gdscript => Some(&GDSCRIPT_SPEC), _ => None, } diff --git a/crates/codegraph-extract/src/lang/solidity.rs b/crates/codegraph-extract/src/lang/solidity.rs new file mode 100644 index 0000000..b465ef7 --- /dev/null +++ b/crates/codegraph-extract/src/lang/solidity.rs @@ -0,0 +1,330 @@ +//! Solidity (`.sol`) `LanguageSpec`. +//! +//! Extraction-tier port of `upstream extraction/languages/solidity.ts` (commit +//! `1441933`, #1170). Backed by the `tree-sitter-solidity` grammar crate, this +//! spec maps the Solidity node kinds to [`codegraph_core::types::NodeKind`]: +//! +//! - `contract_declaration` / `library_declaration` → [`NodeKind::Class`], +//! `interface_declaration` → [`NodeKind::Interface`], +//! `struct_declaration` → [`NodeKind::Struct`], +//! `enum_declaration` → [`NodeKind::Enum`]; +//! - `function_definition` / `modifier_definition` are functions/methods, and the +//! nameless `constructor_definition` / `fallback_receive_definition` get the +//! synthetic names `"constructor"` / `"fallback"` / `"receive"`; +//! - `is`-inheritance is emitted as `Extends` refs by the walker (D3) — the +//! EXISTING resolver promotes those to `Implements` for interface targets, so +//! this spec adds NO resolve-tier code. +//! +//! The Solidity-only AST shapes the generic dispatcher cannot handle +//! (direct-`name` fields, bare-text `enum_value`, header `modifier_invocation`, +//! file-level const, file-level `event`/`error`, and the `user_defined_type` +//! inheritance ancestor) are handled by the `Language::Solidity`-guarded walker +//! extensions in [`crate::walker`]. + +use codegraph_core::types::Language; +use tree_sitter::{Language as TsLanguage, Node}; + +use crate::spec::{ImportInfo, LanguageSpec}; +use crate::walker::{child_by_field, node_text}; + +pub struct SoliditySpec; + +pub static SOLIDITY_SPEC: SoliditySpec = SoliditySpec; + +impl LanguageSpec for SoliditySpec { + fn language(&self) -> Language { + Language::Solidity + } + + fn tree_sitter_language(&self) -> TsLanguage { + tree_sitter_solidity::LANGUAGE.into() + } + + fn function_types(&self) -> &'static [&'static str] { + &["function_definition", "modifier_definition"] + } + + fn class_types(&self) -> &'static [&'static str] { + &["contract_declaration", "library_declaration"] + } + + fn method_types(&self) -> &'static [&'static str] { + &[ + "function_definition", + "modifier_definition", + "constructor_definition", + "fallback_receive_definition", + ] + } + + fn interface_types(&self) -> &'static [&'static str] { + &["interface_declaration"] + } + + fn struct_types(&self) -> &'static [&'static str] { + &["struct_declaration"] + } + + fn enum_types(&self) -> &'static [&'static str] { + &["enum_declaration"] + } + + fn enum_member_types(&self) -> &'static [&'static str] { + &["enum_value"] + } + + fn type_alias_types(&self) -> &'static [&'static str] { + &["user_defined_type_definition"] + } + + fn import_types(&self) -> &'static [&'static str] { + &["import_directive"] + } + + fn call_types(&self) -> &'static [&'static str] { + &[ + "call_expression", + "emit_statement", + "revert_statement", + "modifier_invocation", + ] + } + + fn variable_types(&self) -> &'static [&'static str] { + &[ + "state_variable_declaration", + "constant_variable_declaration", + ] + } + + fn field_types(&self) -> &'static [&'static str] { + &[ + "state_variable_declaration", + "struct_member", + "event_definition", + "error_declaration", + ] + } + + fn name_field(&self) -> &'static str { + "name" + } + + fn body_field(&self) -> &'static str { + "body" + } + + fn params_field(&self) -> &'static str { + "parameters" + } + + fn return_field(&self) -> &'static str { + "return_type" + } + + fn resolve_name(&self, node: Node<'_>, _source: &str) -> Option { + match node.kind() { + "constructor_definition" => Some("constructor".to_string()), + "fallback_receive_definition" => { + // The keyword is an anonymous child (`fallback` or `receive`); + // walk every child and return the first that matches, defaulting + // to `fallback` (upstream `fallbackReceiveName`). + for i in 0..node.child_count() { + if let Some(child) = node.child(i as u32) { + match child.kind() { + "fallback" => return Some("fallback".to_string()), + "receive" => return Some("receive".to_string()), + _ => {} + } + } + } + Some("fallback".to_string()) + } + _ => None, + } + } + + fn get_signature(&self, node: Node<'_>, source: &str) -> Option { + // Params are DIRECT children of the decl node, not a `parameters:` field. + // Walk the named children and reassemble `(p1, p2) `. + let mut params: Vec = Vec::new(); + let mut visibility: Option = None; + let mut mutability: Option = None; + let mut return_type: Option = None; + for child in node.named_children(&mut node.walk()) { + match child.kind() { + "parameter" => params.push(node_text(child, source).trim().to_string()), + "visibility" => visibility = Some(node_text(child, source).trim().to_string()), + "state_mutability" => { + mutability = Some(node_text(child, source).trim().to_string()) + } + "return_type_definition" => { + return_type = Some(node_text(child, source).trim().to_string()) + } + _ => {} + } + } + let mut signature = format!("({})", params.join(", ")); + if let Some(visibility) = visibility { + signature.push(' '); + signature.push_str(&visibility); + } + if let Some(mutability) = mutability { + signature.push(' '); + signature.push_str(&mutability); + } + if let Some(return_type) = return_type { + signature.push(' '); + signature.push_str(&return_type); + } + Some(signature) + } + + fn get_visibility(&self, node: Node<'_>) -> Option { + for child in node.named_children(&mut node.walk()) { + if child.kind() == "visibility" { + let text = child + .child(0) + .map(|kw| kw.kind().to_string()) + .unwrap_or_default(); + return match text.as_str() { + "public" | "external" => Some("public".to_string()), + "private" => Some("private".to_string()), + "internal" => Some("internal".to_string()), + _ => None, + }; + } + } + None + } + + fn is_const(&self, node: Node<'_>) -> bool { + node.kind() == "constant_variable_declaration" + } + + fn extract_import(&self, node: Node<'_>, source: &str) -> Option { + let source_node = child_by_field(node, "source")?; + let module_name = node_text(source_node, source) + .trim() + .trim_matches(['\'', '"']) + .to_string(); + if module_name.is_empty() { + return None; + } + Some(ImportInfo { + module_name, + signature: node_text(node, source).trim().to_string(), + handled_refs: false, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(src: &str) -> tree_sitter::Tree { + let mut parser = tree_sitter::Parser::new(); + parser + .set_language(&tree_sitter_solidity::LANGUAGE.into()) + .unwrap(); + parser.parse(src, None).unwrap() + } + + fn first_of_kind<'t>(node: Node<'t>, kind: &str) -> Option> { + if node.kind() == kind { + return Some(node); + } + for i in 0..node.named_child_count() { + let child = node.named_child(i as u32)?; + if let Some(found) = first_of_kind(child, kind) { + return Some(found); + } + } + None + } + + #[test] + fn solidity_class_types() { + assert_eq!( + SOLIDITY_SPEC.class_types(), + ["contract_declaration", "library_declaration"] + ); + } + + #[test] + fn solidity_call_types_include_emit_revert_modifier() { + assert_eq!( + SOLIDITY_SPEC.call_types(), + [ + "call_expression", + "emit_statement", + "revert_statement", + "modifier_invocation" + ] + ); + } + + #[test] + fn solidity_field_types_include_event_error() { + assert_eq!( + SOLIDITY_SPEC.field_types(), + [ + "state_variable_declaration", + "struct_member", + "event_definition", + "error_declaration" + ] + ); + } + + #[test] + fn solidity_parses_contract() { + let src = "contract A is B { function f() public {} }"; + let tree = parse(src); + let contract = first_of_kind(tree.root_node(), "contract_declaration") + .expect("tree-sitter-solidity parses a contract"); + let name = child_by_field(contract, "name").expect("contract has a name field"); + assert_eq!(node_text(name, src), "A"); + assert!( + first_of_kind(tree.root_node(), "inheritance_specifier").is_some(), + "the `is B` clause parses as an inheritance_specifier" + ); + } + + #[test] + fn solidity_resolve_name_constructor() { + let src = "contract C { constructor() {} }"; + let tree = parse(src); + let ctor = first_of_kind(tree.root_node(), "constructor_definition") + .expect("parses a constructor_definition"); + assert_eq!( + SOLIDITY_SPEC.resolve_name(ctor, src), + Some("constructor".to_string()) + ); + } + + #[test] + fn solidity_resolve_name_fallback_receive() { + let src = "contract C { fallback() external {} receive() external payable {} }"; + let tree = parse(src); + let root = tree.root_node(); + let mut names = Vec::new(); + collect_kind(root, "fallback_receive_definition", &mut names, src); + assert!(names.contains(&"fallback".to_string())); + assert!(names.contains(&"receive".to_string())); + } + + fn collect_kind(node: Node<'_>, kind: &str, out: &mut Vec, src: &str) { + if node.kind() == kind { + if let Some(name) = SOLIDITY_SPEC.resolve_name(node, src) { + out.push(name); + } + } + for i in 0..node.named_child_count() { + if let Some(child) = node.named_child(i as u32) { + collect_kind(child, kind, out, src); + } + } + } +} diff --git a/crates/codegraph-extract/src/walker.rs b/crates/codegraph-extract/src/walker.rs index 3d4805e..328e548 100644 --- a/crates/codegraph-extract/src/walker.rs +++ b/crates/codegraph-extract/src/walker.rs @@ -228,10 +228,39 @@ impl<'a, 'tree> TreeSitterWalker<'a, 'tree> { Language::R => self.visit_r_node(node), Language::Gdscript => self.visit_gdscript_node(node), Language::Cpp => self.visit_cpp_node(node), + Language::Solidity => self.visit_solidity_node(node), _ => false, } } + fn visit_solidity_node(&mut self, node: SyntaxNode<'tree>) -> bool { + // #1170 — upstream `solidityExtractor.visitNode` emits a `field` node for + // `event_definition` / `error_declaration` UNCONDITIONALLY, file-level + // included. The generic field dispatch is class-like-scope-gated, so a + // FREE (top-level) event/error is dropped. Emit the Field here only when + // NOT inside a class-like scope; inside a contract, return false so the D5 + // field path owns it (no double-emit). + if matches!(node.kind(), "event_definition" | "error_declaration") + && !self.is_inside_class_like_node() + { + if let Some(name_node) = child_by_field(node, "name") { + let name = node_text(name_node, self.source); + let signature = property_or_field_signature(node, &name, self.source); + self.create_node( + NodeKind::Field, + &name, + node, + NodeExtra { + signature, + ..NodeExtra::default() + }, + ); + } + return true; + } + false + } + fn visit_cpp_node(&mut self, node: SyntaxNode<'tree>) -> bool { if node.kind() != "namespace_definition" { return false; @@ -1441,6 +1470,17 @@ impl<'a, 'tree> TreeSitterWalker<'a, 'tree> { } } } + // #1170 — Solidity `enum_value` is a LEAF named node (no name field, no id + // child); its own text is the member name. Emit from the node text when + // the name-field / id-child paths above found nothing. + if node.kind() == "enum_value" { + self.create_node( + NodeKind::EnumMember, + &node_text(node, self.source), + node, + NodeExtra::default(), + ); + } } fn extract_type_alias(&mut self, node: SyntaxNode<'tree>) { @@ -1532,6 +1572,29 @@ impl<'a, 'tree> TreeSitterWalker<'a, 'tree> { self.extract_go_variable(node); return; } + if self.spec.language() == Language::Solidity { + // #1170 — file-scope `constant_variable_declaration` (`uint256 constant + // MAX = 100;`) carries a direct `name` field. Emit a Constant (const) + // else Variable. `state_variable_declaration` is always a contract + // member (handled as a Field), so it never reaches this top-level arm. + if let Some(name_node) = child_by_field(node, "name") { + let kind = if self.spec.is_const(node) { + NodeKind::Constant + } else { + NodeKind::Variable + }; + self.create_node( + kind, + &node_text(name_node, self.source), + node, + NodeExtra { + docstring: self.preceding_docstring(node), + ..NodeExtra::default() + }, + ); + } + return; + } if !matches!( self.spec.language(), Language::TypeScript | Language::Tsx | Language::JavaScript | Language::Jsx @@ -1758,6 +1821,29 @@ impl<'a, 'tree> TreeSitterWalker<'a, 'tree> { fn extract_field(&mut self, node: SyntaxNode<'tree>) { let declarators = field_declarators(node).collect::>(); + if declarators.is_empty() && self.spec.language() == Language::Solidity { + // #1170 — Solidity `state_variable_declaration` / `struct_member` / + // `event_definition` / `error_declaration` carry the name in a DIRECT + // `name` field, not a `variable_declarator`, so `field_declarators` + // finds nothing. Emit one Field from the `name` field. + if let Some(name_node) = child_by_field(node, "name") { + let name = node_text(name_node, self.source); + let signature = property_or_field_signature(node, &name, self.source); + if let Some(field_node) = self.create_node( + NodeKind::Field, + &name, + node, + NodeExtra { + signature, + visibility: self.spec.get_visibility(node), + ..NodeExtra::default() + }, + ) { + self.extract_type_annotations(node, &field_node.id); + } + } + return; + } if declarators.is_empty() && self.spec.language() == Language::Php { for elem in node .named_children(&mut node.walk()) @@ -2440,6 +2526,29 @@ impl<'a, 'tree> TreeSitterWalker<'a, 'tree> { type_id, ); } + // #1170 — Solidity `contract Token is IERC20, Ownable`: the + // inheritance_specifier's `ancestor` field is a `user_defined_type` + // (not the Swift `user_type`), whose LAST `identifier` descendant is + // the base name. Shape-guarded on `user_defined_type` so the Swift + // path above is untouched. The resolver promotes Extends→Implements + // for interface targets. + if let Some(base) = child + .named_children(&mut child.walk()) + .find(|c| c.kind() == "user_defined_type") + { + if let Some(name_node) = base + .named_children(&mut base.walk()) + .filter(|c| c.kind() == "identifier") + .last() + { + self.push_ref( + class_id, + &node_text(name_node, self.source), + EdgeKind::Extends, + name_node, + ); + } + } } if child.kind() == "class_heritage" { self.extract_inheritance(child, class_id); @@ -2504,6 +2613,23 @@ impl<'a, 'tree> TreeSitterWalker<'a, 'tree> { fn extract_decorators_for(&mut self, decl_node: SyntaxNode<'tree>, decorated_id: &str) { for child in decl_node.named_children(&mut decl_node.walk()) { self.consider_decorator(child, decorated_id); + // #1170 — a Solidity header `modifier_invocation` (`function f() onlyOwner`, + // `constructor() ERC20(...)`) is a named child of the decl outside the + // `body` field, so the body walker never reaches it. Emit a Calls ref + // (NOT Decorates) named by its first identifier so flow traversal rides + // the modifier guard / base-constructor chain. Shape-guarded so other + // languages are untouched. + if child.kind() == "modifier_invocation" { + if let Some(ident) = child + .named_children(&mut child.walk()) + .find(|c| c.kind() == "identifier") + { + let name = node_text(ident, self.source); + if !name.is_empty() { + self.push_ref(decorated_id, &name, EdgeKind::Calls, child); + } + } + } // tree-sitter.ts:2940-2948 — Java/Kotlin/C#/Swift annotations and // attributes nest INSIDE a `modifiers` child; descend one level so // they are not silently dropped. @@ -3541,6 +3667,134 @@ class Model {} assert!(has_ref(&refs, EdgeKind::Calls, "helper")); } + // ---- Solidity (.sol) extraction tier (#1170) ---- + + #[test] + fn solidity_contract_is_records_extends() { + let src = "contract MyToken is Token, IERC20 { }"; + let (_, refs) = run("A.sol", src, Language::Solidity); + assert!(has_ref(&refs, EdgeKind::Extends, "Token")); + assert!(has_ref(&refs, EdgeKind::Extends, "IERC20")); + } + + #[test] + fn solidity_state_var_and_struct_member_are_fields() { + let src = r#" +contract C { + uint256 public total; + struct S { uint256 a; } +} +"#; + let (nodes, _) = run("C.sol", src, Language::Solidity); + assert!(has_node(&nodes, NodeKind::Field, "total")); + assert!(has_node(&nodes, NodeKind::Field, "a")); + } + + #[test] + fn solidity_event_error_are_field_nodes() { + let src = r#" +contract C { + event Transfer(address to, uint256 v); + error Bad(uint256 code); +} +"#; + let (nodes, _) = run("C.sol", src, Language::Solidity); + assert!(has_node(&nodes, NodeKind::Field, "Transfer")); + assert!(has_node(&nodes, NodeKind::Field, "Bad")); + } + + #[test] + fn solidity_enum_values_are_members() { + let src = "contract C { enum Status { Active, Closed } }"; + let (nodes, _) = run("C.sol", src, Language::Solidity); + assert!(has_node(&nodes, NodeKind::EnumMember, "Active")); + assert!(has_node(&nodes, NodeKind::EnumMember, "Closed")); + } + + #[test] + fn solidity_modifier_guard_is_call() { + let src = r#" +contract C { + modifier onlyOwner() { _; } + function withdraw() public onlyOwner { } +} +"#; + let (_, refs) = run("C.sol", src, Language::Solidity); + assert!(has_ref(&refs, EdgeKind::Calls, "onlyOwner")); + assert!( + !refs + .iter() + .any(|r| r.reference_name == "onlyOwner" + && r.reference_kind == EdgeKind::Decorates), + "modifier guard must be a Calls ref, not Decorates" + ); + } + + #[test] + fn solidity_top_level_constant_is_constant() { + let src = "uint256 constant MAX = 100;"; + let (nodes, _) = run("C.sol", src, Language::Solidity); + assert!(has_node(&nodes, NodeKind::Constant, "MAX")); + } + + #[test] + fn solidity_file_level_event_error_are_fields() { + let src = r#" +event Bar(uint256 x); +error Foo(address a); +"#; + let (nodes, _) = run("free.sol", src, Language::Solidity); + assert!(has_node(&nodes, NodeKind::Field, "Bar")); + assert!(has_node(&nodes, NodeKind::Field, "Foo")); + } + + #[test] + fn solidity_top_level_event_error_do_not_double_emit_inside_contract() { + let src = "contract C { event E(uint256 x); }"; + let (nodes, _) = run("C.sol", src, Language::Solidity); + let count = nodes + .iter() + .filter(|n| n.kind == NodeKind::Field && n.name == "E") + .count(); + assert_eq!(count, 1, "in-contract event must emit exactly one Field"); + } + + #[test] + fn solidity_extracts_contract_interface_struct_fn_field_event_import_call() { + let src = r#" +import "./IERC20.sol"; + +interface IERC20 { + function transfer(address to, uint256 v) external returns (bool); +} + +contract Token is IERC20 { + event Transfer(address to, uint256 v); + modifier onlyOwner() { _; } + constructor() { } + function transfer(address to, uint256 v) public onlyOwner returns (bool) { + emit Transfer(to, v); + return true; + } +} + +library Math { + function add(uint256 a, uint256 b) internal pure returns (uint256) { return a + b; } +} +"#; + let (nodes, refs) = run("Token.sol", src, Language::Solidity); + assert!(has_node(&nodes, NodeKind::Class, "Token")); + assert!(has_node(&nodes, NodeKind::Interface, "IERC20")); + assert!(has_node(&nodes, NodeKind::Class, "Math")); + assert!(has_node(&nodes, NodeKind::Method, "transfer")); + assert!(has_node(&nodes, NodeKind::Method, "constructor")); + assert!(has_node(&nodes, NodeKind::Field, "Transfer")); + assert!(has_node(&nodes, NodeKind::Import, "./IERC20.sol")); + assert!(has_ref(&refs, EdgeKind::Extends, "IERC20")); + assert!(has_ref(&refs, EdgeKind::Calls, "onlyOwner")); + assert!(has_ref(&refs, EdgeKind::Calls, "Transfer")); + } + // ---- Lua / Luau require variants ---- #[test] diff --git a/crates/codegraph-store/src/queries.rs b/crates/codegraph-store/src/queries.rs index f6bdcc3..6e16dc1 100644 --- a/crates/codegraph-store/src/queries.rs +++ b/crates/codegraph-store/src/queries.rs @@ -1524,6 +1524,7 @@ fn parse_language(value: String) -> rusqlite::Result { "luau" => Language::Luau, "objc" => Language::ObjC, "r" => Language::R, + "solidity" => Language::Solidity, "yaml" => Language::Yaml, "twig" => Language::Twig, "xml" => Language::Xml, diff --git a/docs/equivalence.md b/docs/equivalence.md index 9936318..00a8b6a 100644 --- a/docs/equivalence.md +++ b/docs/equivalence.md @@ -406,6 +406,56 @@ The `generated_golden_matches_committed_arkts_fixture` and `arkts_db_is_self_equivalent_to_arkts_golden` tests in `crates/codegraph-bench/tests/equivalence.rs` enforce byte-stability. +### Solidity fixture + +An eighth golden fixture, `reference/golden/solidity/`, guards Solidity (`.sol`) +extraction (upstream #1170 / `1441933`). Solidity is a **new `Language::Solidity` +variant** backed by a **dedicated `tree-sitter-solidity` grammar**. `.sol` maps to +`Language::Solidity`. The corpus (`crates/codegraph-bench/fixtures/solidity/`) has +an `IERC20.sol` interface and a `Token.sol` that imports it, declares a file-level +`error` and a file-level `constant`, and a `contract Token is IERC20` carrying a +state variable, an `event`, an `enum`, a `struct`, a `modifier`, a `constructor`, +`fallback`/`receive`, and a `transfer` function guarded by the modifier that +`emit`s the event, plus a `library Math`. What it guards: + +- both `.sol` files with `"language": "solidity"`; +- `contract Token` / `library Math` as `NodeKind::Class`, `interface IERC20` as + `NodeKind::Interface`, `struct Holder` as `NodeKind::Struct`, `enum Status` as + `NodeKind::Enum` with its `Active`/`Closed` members (bare-text `enum_value`); +- functions/modifiers/methods, including the synthetic `constructor` / `fallback` + / `receive` method names (nameless grammar nodes); +- state variable / struct member / `event` / `error` as `NodeKind::Field` name + nodes (direct-`name` field, no `variable_declarator`), including the file-level + `Unauthorized` error and the file-level `MAX_SUPPLY` constant; +- the `./IERC20.sol` import node + `imports` edge; +- `is`-inheritance emitted as an `Extends` ref, promoted by the EXISTING resolver + to an `Implements` edge `Token → IERC20` (interface target, present in-corpus); +- `emit`/header `modifier_invocation` `Calls` edges (`transfer → Transfer`, + `transfer → onlyOwner`), resolved to same-file targets. + +Because the fixture is fully self-contained, every ref resolves in-corpus, so +`refs.json` is empty and `edges.json` holds only RESOLVED edges — the expected +post-resolution state. No `FrameworkResolver` impl is involved; the +`Extends → Implements` promotion is the same path Java/C# use +(`resolver.rs:1231-1247`). Adding the variant is byte-neutral for +`colby.schema.sql` (language is a stored TEXT value, not DDL) and for the seven +existing goldens (none holds a `.sol` file). + +Regenerate reproducibly (identical recipe to the ArkTS fixture, substituting +`solidity`): + +```bash +rm -rf /tmp/cg-fixture-solidity && cp -r crates/codegraph-bench/fixtures/solidity /tmp/cg-fixture-solidity +cargo build --release -p codegraph-rs +CODEGRAPH_NO_DAEMON=1 CODEGRAPH_NO_WATCH=1 ./target/release/codegraph init /tmp/cg-fixture-solidity +cp /tmp/cg-fixture-solidity/.codegraph/codegraph.db reference/golden/solidity/colby.db +cargo run -p codegraph-bench --bin bench -- --gen-golden reference/golden/solidity/colby.db reference/golden/solidity +``` + +The `generated_golden_matches_committed_solidity_fixture` and +`solidity_db_is_self_equivalent_to_solidity_golden` tests in +`crates/codegraph-bench/tests/equivalence.rs` enforce byte-stability. + ### KNOWN_DIFFS.md format Tier-3 differences are allowlisted by grep-able lines in repo-root diff --git a/docs/languages.md b/docs/languages.md index 630a494..4a18e7e 100644 --- a/docs/languages.md +++ b/docs/languages.md @@ -4,7 +4,7 @@ CodeGraph extracts code structure deterministically using tree-sitter grammars a embedded extractors. No AI, vectors, or embeddings are involved. The output is byte-stable across runs. -**33 concrete languages** are supported, grouped into three extraction tiers based on what +**34 concrete languages** are supported, grouped into three extraction tiers based on what the extractor produces. > **Note on TypeScript/JavaScript variants:** `typescript` and `tsx`, and `javascript` and @@ -14,38 +14,39 @@ the extractor produces. --- -## Tier 1 — Full symbol extraction (24 languages) +## Tier 1 — Full symbol extraction (25 languages) Tree-sitter parses the file and extracts all symbols (functions, classes, structs, methods, variables, imports, etc.) plus call and dependency edges. This is the richest extraction level. -| Language | Extensions | Extraction | Notes | -| ----------- | ------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| TypeScript | `.ts` `.mts` `.cts` | Full tree-sitter | | -| TSX | `.tsx` | Full tree-sitter | TypeScript grammar, JSX syntax | -| JavaScript | `.js` `.mjs` `.cjs` `.xsjs` `.xsjslib` | Full tree-sitter | | -| JSX | `.jsx` | Full tree-sitter | JavaScript grammar, JSX syntax | -| ArkTS | `.ets` | Full tree-sitter | HarmonyOS / OpenHarmony; `tree-sitter-arkts` grammar. `@Component struct` → struct symbol. ArkUI dynamic-dispatch bridges deferred. Plain `.ts` stays TypeScript | -| Python | `.py` `.pyw` | Full tree-sitter | | -| Go | `.go` | Full tree-sitter | | -| Rust | `.rs` | Full tree-sitter | | -| Java | `.java` | Full tree-sitter | | -| C | `.c` `.h` | Full tree-sitter | `.h` may be promoted to C++ or Objective-C by heuristics | -| C++ | `.cpp` `.cc` `.cxx` `.hpp` `.hxx` | Full tree-sitter | | -| C# | `.cs` | Full tree-sitter | | -| PHP | `.php` `.module` `.install` `.theme` `.inc` | Full tree-sitter | | -| Ruby | `.rb` `.rake` | Full tree-sitter | | -| Swift | `.swift` | Full tree-sitter | | -| Kotlin | `.kt` `.kts` | Full tree-sitter | | -| Dart | `.dart` | Full tree-sitter | | -| Scala | `.scala` `.sc` | Full tree-sitter | | -| Lua | `.lua` | Full tree-sitter | | -| Luau | `.luau` | Full tree-sitter | Roblox Luau dialect | -| Objective-C | `.m` `.mm` | Full tree-sitter | | -| R | `.r` | Full tree-sitter | | -| GDScript | `.gd` | Full tree-sitter | Godot scripting; extracts functions, classes, enums, variables, signals, extends, preload. Dynamic dispatch edges (connect/get_node/$/%/call/group) added by the Godot resolver — see [`docs/godot.md`](godot.md) | -| Pascal | `.pas` `.dpr` `.dpk` `.lpr` `.dfm` `.fmx` | Full tree-sitter / custom | `.dfm`/`.fmx` form files use a custom path | +| Language | Extensions | Extraction | Notes | +| ----------- | ------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| TypeScript | `.ts` `.mts` `.cts` | Full tree-sitter | | +| TSX | `.tsx` | Full tree-sitter | TypeScript grammar, JSX syntax | +| JavaScript | `.js` `.mjs` `.cjs` `.xsjs` `.xsjslib` | Full tree-sitter | | +| JSX | `.jsx` | Full tree-sitter | JavaScript grammar, JSX syntax | +| ArkTS | `.ets` | Full tree-sitter | HarmonyOS / OpenHarmony; `tree-sitter-arkts` grammar. `@Component struct` → struct symbol. ArkUI dynamic-dispatch bridges deferred. Plain `.ts` stays TypeScript | +| Python | `.py` `.pyw` | Full tree-sitter | | +| Go | `.go` | Full tree-sitter | | +| Rust | `.rs` | Full tree-sitter | | +| Java | `.java` | Full tree-sitter | | +| C | `.c` `.h` | Full tree-sitter | `.h` may be promoted to C++ or Objective-C by heuristics | +| C++ | `.cpp` `.cc` `.cxx` `.hpp` `.hxx` | Full tree-sitter | | +| C# | `.cs` | Full tree-sitter | | +| PHP | `.php` `.module` `.install` `.theme` `.inc` | Full tree-sitter | | +| Ruby | `.rb` `.rake` | Full tree-sitter | | +| Swift | `.swift` | Full tree-sitter | | +| Kotlin | `.kt` `.kts` | Full tree-sitter | | +| Dart | `.dart` | Full tree-sitter | | +| Scala | `.scala` `.sc` | Full tree-sitter | | +| Lua | `.lua` | Full tree-sitter | | +| Luau | `.luau` | Full tree-sitter | Roblox Luau dialect | +| Objective-C | `.m` `.mm` | Full tree-sitter | | +| R | `.r` | Full tree-sitter | | +| Solidity | `.sol` | Full tree-sitter | `tree-sitter-solidity` grammar; contracts/libraries/interfaces, structs, enums, modifiers, events, errors; `is`-inheritance → Extends (resolver promotes to Implements for interfaces); emit/revert/modifier-guard call edges | +| GDScript | `.gd` | Full tree-sitter | Godot scripting; extracts functions, classes, enums, variables, signals, extends, preload. Dynamic dispatch edges (connect/get_node/$/%/call/group) added by the Godot resolver — see [`docs/godot.md`](godot.md) | +| Pascal | `.pas` `.dpr` `.dpk` `.lpr` `.dfm` `.fmx` | Full tree-sitter / custom | `.dfm`/`.fmx` form files use a custom path | --- diff --git a/reference/golden/solidity/colby.db b/reference/golden/solidity/colby.db new file mode 100644 index 0000000..a786dde Binary files /dev/null and b/reference/golden/solidity/colby.db differ diff --git a/reference/golden/solidity/edges.json b/reference/golden/solidity/edges.json new file mode 100644 index 0000000..e638d9d --- /dev/null +++ b/reference/golden/solidity/edges.json @@ -0,0 +1,239 @@ +[ + { + "col": 0, + "kind": "imports", + "line": 4, + "metadata": { + "confidence": 0.7, + "resolvedBy": "file-path" + }, + "provenance": null, + "source": "file:Token.sol", + "target": "file:IERC20.sol" + }, + { + "col": 18, + "kind": "implements", + "line": 10, + "metadata": { + "confidence": 0.9, + "resolvedBy": "exact-match" + }, + "provenance": null, + "source": "class:cbe42c3edda8b791e2f65c50de43bf90", + "target": "interface:cc05f27c43c35cc4ab3d86bb6f06872c" + }, + { + "col": 56, + "kind": "calls", + "line": 39, + "metadata": { + "confidence": 0.9, + "resolvedBy": "exact-match" + }, + "provenance": null, + "source": "method:1c8a7b45450c9d4eed6e478286852ad3", + "target": "method:d56cce424b1c30328b00f0b981df8233" + }, + { + "col": 8, + "kind": "calls", + "line": 40, + "metadata": { + "confidence": 0.9, + "resolvedBy": "exact-match" + }, + "provenance": null, + "source": "method:1c8a7b45450c9d4eed6e478286852ad3", + "target": "field:55aa8c4f588a7ddd89448639dd5cc613" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "class:6a57b726319c85287cd5966662055efd", + "target": "method:01522a942d17de1001d5a8e65bbe316a" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "class:cbe42c3edda8b791e2f65c50de43bf90", + "target": "enum:18a0bcec3e0b273f8ca328dc05fc7c62" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "class:cbe42c3edda8b791e2f65c50de43bf90", + "target": "field:55aa8c4f588a7ddd89448639dd5cc613" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "class:cbe42c3edda8b791e2f65c50de43bf90", + "target": "field:620dcea68e269e5d25084e930b381daa" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "class:cbe42c3edda8b791e2f65c50de43bf90", + "target": "method:069c78f8d4d5c513593c3c944014a0b0" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "class:cbe42c3edda8b791e2f65c50de43bf90", + "target": "method:1c8a7b45450c9d4eed6e478286852ad3" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "class:cbe42c3edda8b791e2f65c50de43bf90", + "target": "method:46d4bf9937e477f5bd270bb05b6696f0" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "class:cbe42c3edda8b791e2f65c50de43bf90", + "target": "method:768b3877a930b3ee9c3b24466c601f38" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "class:cbe42c3edda8b791e2f65c50de43bf90", + "target": "method:d56cce424b1c30328b00f0b981df8233" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "class:cbe42c3edda8b791e2f65c50de43bf90", + "target": "struct:6879c4fba147cd44c382552884de5563" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "enum:18a0bcec3e0b273f8ca328dc05fc7c62", + "target": "enum_member:433d4857330d95a2c514d9a3e682a08b" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "enum:18a0bcec3e0b273f8ca328dc05fc7c62", + "target": "enum_member:e933e9c8a19aefff4e435ab2db55b0ae" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:IERC20.sol", + "target": "interface:cc05f27c43c35cc4ab3d86bb6f06872c" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:Token.sol", + "target": "class:6a57b726319c85287cd5966662055efd" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:Token.sol", + "target": "class:cbe42c3edda8b791e2f65c50de43bf90" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:Token.sol", + "target": "constant:2e4331f08e0c9bb2d6c0c9de9f696935" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:Token.sol", + "target": "field:4df8f64fd61911a088da5b8596f81045" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:Token.sol", + "target": "import:a2e94e572fe1823c82b046dd0a3030d9" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "interface:cc05f27c43c35cc4ab3d86bb6f06872c", + "target": "method:14e6265384fe621344552d55e10f8466" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "struct:6879c4fba147cd44c382552884de5563", + "target": "field:ee915f379fbf10ac1ed613b353ccabc0" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "struct:6879c4fba147cd44c382552884de5563", + "target": "field:fcc8c476605ce6c1cdd89fdf53a1f4fc" + } +] diff --git a/reference/golden/solidity/files.json b/reference/golden/solidity/files.json new file mode 100644 index 0000000..d14c806 --- /dev/null +++ b/reference/golden/solidity/files.json @@ -0,0 +1,18 @@ +[ + { + "content_hash": "bef3d1355e28323c5eaeeac93fd3693297706de47dd276566eee9d64a4568d6f", + "errors": null, + "language": "solidity", + "node_count": 3, + "path": "IERC20.sol", + "size": 152 + }, + { + "content_hash": "c8976b8cc5a7ea552da5c19efd79afa395873bd8a1253ebe9282b538e4c9a8e3", + "errors": null, + "language": "solidity", + "node_count": 20, + "path": "Token.sol", + "size": 841 + } +] diff --git a/reference/golden/solidity/nodes.json b/reference/golden/solidity/nodes.json new file mode 100644 index 0000000..9e4e732 --- /dev/null +++ b/reference/golden/solidity/nodes.json @@ -0,0 +1,508 @@ +[ + { + "decorators": null, + "docstring": null, + "end_column": 1, + "end_line": 49, + "file_path": "Token.sol", + "id": "class:6a57b726319c85287cd5966662055efd", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "class", + "language": "solidity", + "name": "Math", + "qualified_name": "Math", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 45, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 1, + "end_line": 43, + "file_path": "Token.sol", + "id": "class:cbe42c3edda8b791e2f65c50de43bf90", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "class", + "language": "solidity", + "name": "Token", + "qualified_name": "Token", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 10, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 38, + "end_line": 8, + "file_path": "Token.sol", + "id": "constant:2e4331f08e0c9bb2d6c0c9de9f696935", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "constant", + "language": "solidity", + "name": "MAX_SUPPLY", + "qualified_name": "MAX_SUPPLY", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 8, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 5, + "end_line": 18, + "file_path": "Token.sol", + "id": "enum:18a0bcec3e0b273f8ca328dc05fc7c62", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "enum", + "language": "solidity", + "name": "Status", + "qualified_name": "Token::Status", + "return_type": null, + "signature": null, + "start_column": 4, + "start_line": 15, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 14, + "end_line": 16, + "file_path": "Token.sol", + "id": "enum_member:433d4857330d95a2c514d9a3e682a08b", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "enum_member", + "language": "solidity", + "name": "Active", + "qualified_name": "Token::Status::Active", + "return_type": null, + "signature": null, + "start_column": 8, + "start_line": 16, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 14, + "end_line": 17, + "file_path": "Token.sol", + "id": "enum_member:e933e9c8a19aefff4e435ab2db55b0ae", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "enum_member", + "language": "solidity", + "name": "Closed", + "qualified_name": "Token::Status::Closed", + "return_type": null, + "signature": null, + "start_column": 8, + "start_line": 17, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 35, + "end_line": 6, + "file_path": "Token.sol", + "id": "field:4df8f64fd61911a088da5b8596f81045", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "field", + "language": "solidity", + "name": "Unauthorized", + "qualified_name": "Unauthorized", + "return_type": null, + "signature": "address caller Unauthorized", + "start_column": 0, + "start_line": 6, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 56, + "end_line": 13, + "file_path": "Token.sol", + "id": "field:55aa8c4f588a7ddd89448639dd5cc613", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "field", + "language": "solidity", + "name": "Transfer", + "qualified_name": "Token::Transfer", + "return_type": null, + "signature": "address indexed from Transfer", + "start_column": 4, + "start_line": 13, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 25, + "end_line": 11, + "file_path": "Token.sol", + "id": "field:620dcea68e269e5d25084e930b381daa", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "field", + "language": "solidity", + "name": "total", + "qualified_name": "Token::total", + "return_type": null, + "signature": "uint256 total", + "start_column": 4, + "start_line": 11, + "type_parameters": null, + "visibility": "public" + }, + { + "decorators": null, + "docstring": null, + "end_column": 24, + "end_line": 22, + "file_path": "Token.sol", + "id": "field:ee915f379fbf10ac1ed613b353ccabc0", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "field", + "language": "solidity", + "name": "balance", + "qualified_name": "Token::Holder::balance", + "return_type": null, + "signature": "uint256 balance", + "start_column": 8, + "start_line": 22, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 24, + "end_line": 21, + "file_path": "Token.sol", + "id": "field:fcc8c476605ce6c1cdd89fdf53a1f4fc", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "field", + "language": "solidity", + "name": "account", + "qualified_name": "Token::Holder::account", + "return_type": null, + "signature": "address account", + "start_column": 8, + "start_line": 21, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 0, + "end_line": 7, + "file_path": "IERC20.sol", + "id": "file:IERC20.sol", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "file", + "language": "solidity", + "name": "IERC20.sol", + "qualified_name": "IERC20.sol", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 1, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 0, + "end_line": 50, + "file_path": "Token.sol", + "id": "file:Token.sol", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "file", + "language": "solidity", + "name": "Token.sol", + "qualified_name": "Token.sol", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 1, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 22, + "end_line": 4, + "file_path": "Token.sol", + "id": "import:a2e94e572fe1823c82b046dd0a3030d9", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "import", + "language": "solidity", + "name": "./IERC20.sol", + "qualified_name": "./IERC20.sol", + "return_type": null, + "signature": "import \"./IERC20.sol\";", + "start_column": 0, + "start_line": 4, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 1, + "end_line": 6, + "file_path": "IERC20.sol", + "id": "interface:cc05f27c43c35cc4ab3d86bb6f06872c", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "interface", + "language": "solidity", + "name": "IERC20", + "qualified_name": "IERC20", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 4, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 5, + "end_line": 48, + "file_path": "Token.sol", + "id": "method:01522a942d17de1001d5a8e65bbe316a", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "method", + "language": "solidity", + "name": "add", + "qualified_name": "Math::add", + "return_type": null, + "signature": "(uint256 a, uint256 b) internal pure returns (uint256)", + "start_column": 4, + "start_line": 46, + "type_parameters": null, + "visibility": "internal" + }, + { + "decorators": null, + "docstring": null, + "end_column": 5, + "end_line": 34, + "file_path": "Token.sol", + "id": "method:069c78f8d4d5c513593c3c944014a0b0", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "method", + "language": "solidity", + "name": "fallback", + "qualified_name": "Token::fallback", + "return_type": null, + "signature": "() external", + "start_column": 4, + "start_line": 33, + "type_parameters": null, + "visibility": "public" + }, + { + "decorators": null, + "docstring": null, + "end_column": 73, + "end_line": 5, + "file_path": "IERC20.sol", + "id": "method:14e6265384fe621344552d55e10f8466", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "method", + "language": "solidity", + "name": "transfer", + "qualified_name": "IERC20::transfer", + "return_type": null, + "signature": "(address to, uint256 value) external returns (bool)", + "start_column": 4, + "start_line": 5, + "type_parameters": null, + "visibility": "public" + }, + { + "decorators": null, + "docstring": null, + "end_column": 5, + "end_line": 42, + "file_path": "Token.sol", + "id": "method:1c8a7b45450c9d4eed6e478286852ad3", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "method", + "language": "solidity", + "name": "transfer", + "qualified_name": "Token::transfer", + "return_type": null, + "signature": "(address to, uint256 value) public returns (bool)", + "start_column": 4, + "start_line": 39, + "type_parameters": null, + "visibility": "public" + }, + { + "decorators": null, + "docstring": null, + "end_column": 5, + "end_line": 31, + "file_path": "Token.sol", + "id": "method:46d4bf9937e477f5bd270bb05b6696f0", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "method", + "language": "solidity", + "name": "constructor", + "qualified_name": "Token::constructor", + "return_type": null, + "signature": "()", + "start_column": 4, + "start_line": 29, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 5, + "end_line": 37, + "file_path": "Token.sol", + "id": "method:768b3877a930b3ee9c3b24466c601f38", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "method", + "language": "solidity", + "name": "receive", + "qualified_name": "Token::receive", + "return_type": null, + "signature": "() external payable", + "start_column": 4, + "start_line": 36, + "type_parameters": null, + "visibility": "public" + }, + { + "decorators": null, + "docstring": null, + "end_column": 5, + "end_line": 27, + "file_path": "Token.sol", + "id": "method:d56cce424b1c30328b00f0b981df8233", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "method", + "language": "solidity", + "name": "onlyOwner", + "qualified_name": "Token::onlyOwner", + "return_type": null, + "signature": "()", + "start_column": 4, + "start_line": 25, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 5, + "end_line": 23, + "file_path": "Token.sol", + "id": "struct:6879c4fba147cd44c382552884de5563", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "struct", + "language": "solidity", + "name": "Holder", + "qualified_name": "Token::Holder", + "return_type": null, + "signature": null, + "start_column": 4, + "start_line": 20, + "type_parameters": null, + "visibility": null + } +] diff --git a/reference/golden/solidity/refs.json b/reference/golden/solidity/refs.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/reference/golden/solidity/refs.json @@ -0,0 +1 @@ +[] diff --git a/reference/golden/solidity/schema.sql b/reference/golden/solidity/schema.sql new file mode 100644 index 0000000..d36218f --- /dev/null +++ b/reference/golden/solidity/schema.sql @@ -0,0 +1,117 @@ +CREATE TABLE schema_versions ( +version INTEGER PRIMARY KEY, +applied_at INTEGER NOT NULL, +description TEXT +); +CREATE TABLE nodes ( +id TEXT PRIMARY KEY, +kind TEXT NOT NULL, +name TEXT NOT NULL, +qualified_name TEXT NOT NULL, +file_path TEXT NOT NULL, +language TEXT NOT NULL, +start_line INTEGER NOT NULL, +end_line INTEGER NOT NULL, +start_column INTEGER NOT NULL, +end_column INTEGER NOT NULL, +docstring TEXT, +signature TEXT, +visibility TEXT, +is_exported INTEGER DEFAULT 0, +is_async INTEGER DEFAULT 0, +is_static INTEGER DEFAULT 0, +is_abstract INTEGER DEFAULT 0, +decorators TEXT, -- JSON array +type_parameters TEXT, -- JSON array +return_type TEXT, -- normalized return/result type name (e.g. C++ method return, for receiver-type inference) +updated_at INTEGER NOT NULL +); +CREATE TABLE edges ( +id INTEGER PRIMARY KEY AUTOINCREMENT, +source TEXT NOT NULL, +target TEXT NOT NULL, +kind TEXT NOT NULL, +metadata TEXT, -- JSON object +line INTEGER, +col INTEGER, +provenance TEXT DEFAULT NULL, +FOREIGN KEY (source) REFERENCES nodes(id) ON DELETE CASCADE, +FOREIGN KEY (target) REFERENCES nodes(id) ON DELETE CASCADE +); +CREATE TABLE sqlite_sequence(name,seq); +CREATE TABLE files ( +path TEXT PRIMARY KEY, +content_hash TEXT NOT NULL, +language TEXT NOT NULL, +size INTEGER NOT NULL, +modified_at INTEGER NOT NULL, +indexed_at INTEGER NOT NULL, +node_count INTEGER DEFAULT 0, +errors TEXT -- JSON array +); +CREATE TABLE unresolved_refs ( +id INTEGER PRIMARY KEY AUTOINCREMENT, +from_node_id TEXT NOT NULL, +reference_name TEXT NOT NULL, +reference_kind TEXT NOT NULL, +line INTEGER NOT NULL, +col INTEGER NOT NULL, +candidates TEXT, -- JSON array +file_path TEXT NOT NULL DEFAULT '', +language TEXT NOT NULL DEFAULT 'unknown', +reference_subkind TEXT, +FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE +); +CREATE INDEX idx_nodes_kind ON nodes(kind); +CREATE INDEX idx_nodes_name ON nodes(name); +CREATE INDEX idx_nodes_qualified_name ON nodes(qualified_name); +CREATE INDEX idx_nodes_file_path ON nodes(file_path); +CREATE INDEX idx_nodes_language ON nodes(language); +CREATE INDEX idx_nodes_file_line ON nodes(file_path, start_line); +CREATE INDEX idx_nodes_lower_name ON nodes(lower(name)); +CREATE VIRTUAL TABLE nodes_fts USING fts5( +id, +name, +qualified_name, +docstring, +signature, +content='nodes', +content_rowid='rowid' +) +/* nodes_fts(id,name,qualified_name,docstring,signature) */; +CREATE TABLE 'nodes_fts_data'(id INTEGER PRIMARY KEY, block BLOB); +CREATE TABLE 'nodes_fts_idx'(segid, term, pgno, PRIMARY KEY(segid, term)) WITHOUT ROWID; +CREATE TABLE 'nodes_fts_docsize'(id INTEGER PRIMARY KEY, sz BLOB); +CREATE TABLE 'nodes_fts_config'(k PRIMARY KEY, v) WITHOUT ROWID; +CREATE TRIGGER nodes_ai AFTER INSERT ON nodes BEGIN +INSERT INTO nodes_fts(rowid, id, name, qualified_name, docstring, signature) +VALUES (NEW.rowid, NEW.id, NEW.name, NEW.qualified_name, NEW.docstring, NEW.signature); +END; +CREATE TRIGGER nodes_ad AFTER DELETE ON nodes BEGIN +INSERT INTO nodes_fts(nodes_fts, rowid, id, name, qualified_name, docstring, signature) +VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.qualified_name, OLD.docstring, OLD.signature); +END; +CREATE TRIGGER nodes_au AFTER UPDATE ON nodes BEGIN +INSERT INTO nodes_fts(nodes_fts, rowid, id, name, qualified_name, docstring, signature) +VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.qualified_name, OLD.docstring, OLD.signature); +INSERT INTO nodes_fts(rowid, id, name, qualified_name, docstring, signature) +VALUES (NEW.rowid, NEW.id, NEW.name, NEW.qualified_name, NEW.docstring, NEW.signature); +END; +CREATE INDEX idx_edges_kind ON edges(kind); +CREATE INDEX idx_edges_source_kind ON edges(source, kind); +CREATE INDEX idx_edges_target_kind ON edges(target, kind); +CREATE UNIQUE INDEX idx_edges_identity +ON edges(source, target, kind, IFNULL(line, -1), IFNULL(col, -1)); +CREATE INDEX idx_files_language ON files(language); +CREATE INDEX idx_files_modified_at ON files(modified_at); +CREATE INDEX idx_unresolved_from_node ON unresolved_refs(from_node_id); +CREATE INDEX idx_unresolved_name ON unresolved_refs(reference_name); +CREATE INDEX idx_unresolved_file_path ON unresolved_refs(file_path); +CREATE INDEX idx_unresolved_from_name ON unresolved_refs(from_node_id, reference_name); +CREATE INDEX idx_edges_provenance ON edges(provenance); +CREATE TABLE project_metadata ( +key TEXT PRIMARY KEY, +value TEXT NOT NULL, +updated_at INTEGER NOT NULL +); +CREATE TABLE sqlite_stat1(tbl,idx,stat);