diff --git a/graphify/analyze.py b/graphify/analyze.py index df1c206610..fdeeced7e7 100644 --- a/graphify/analyze.py +++ b/graphify/analyze.py @@ -41,6 +41,7 @@ **{e: "dotnet" for e in (".cs",)}, **{e: "php" for e in (".php",)}, **{e: "r" for e in (".r",)}, + **{e: "solidity" for e in (".sol",)}, } diff --git a/graphify/detect.py b/graphify/detect.py index eec8aaf1d2..e0a7c22691 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -41,7 +41,7 @@ class FileType(str, Enum): _MTIME_COARSE_S = 2.0 _MTIME_SUBSECOND_S = 0.05 -CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.ejs', '.ets', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.metal', '.rb', '.rake', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.ml', '.mli', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.svh', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk', '.sh', '.bash', '.json', '.tf', '.tfvars', '.hcl', '.dm', '.dme', '.dmi', '.dmm', '.dmf', '.sln', '.slnx', '.csproj', '.fsproj', '.vbproj', '.xaml', '.razor', '.cshtml', '.cls', '.trigger', '.lisp', '.cl', '.lsp', '.asd', '.robot', '.resource'} +CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.ejs', '.ets', '.go', '.rs', '.sol', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.metal', '.rb', '.rake', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.ml', '.mli', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.svh', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk', '.sh', '.bash', '.json', '.tf', '.tfvars', '.hcl', '.dm', '.dme', '.dmi', '.dmm', '.dmf', '.sln', '.slnx', '.csproj', '.fsproj', '.vbproj', '.xaml', '.razor', '.cshtml', '.cls', '.trigger', '.lisp', '.cl', '.lsp', '.asd', '.robot', '.resource'} DOC_EXTENSIONS = {'.md', '.mdx', '.qmd', '.skill', '.txt', '.rst', '.html', '.yaml', '.yml'} PAPER_EXTENSIONS = {'.pdf'} IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'} diff --git a/graphify/extract.py b/graphify/extract.py index da66f41317..c2c7e37596 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -57,6 +57,10 @@ from graphify.extractors.robot import extract_robot # noqa: F401 from graphify.extractors.rust import extract_rust # noqa: F401 from graphify.extractors.sln import extract_sln # noqa: F401 +from graphify.extractors.solidity import ( # noqa: F401 + extract_solidity, + resolve_solidity_type_references, +) from graphify.extractors.sql import extract_sql # noqa: F401 from graphify.extractors.terraform import extract_terraform, prepare_terraform, resolve_terraform_modules # noqa: F401 from graphify.extractors.verilog import extract_verilog # noqa: F401 @@ -2763,6 +2767,7 @@ def _lang_is_case_insensitive(source_file: object) -> bool: ".py": "python", ".go": "go", ".rs": "rust", + ".sol": "solidity", ".rb": "ruby", ".rake": "ruby", ".php": "php", ".phtml": "php", ".php3": "php", ".php4": "php", ".php5": "php", ".php7": "php", ".phps": "php", @@ -5194,6 +5199,11 @@ def _resolve_kotlin_member_calls( register_language_resolver( LanguageResolver("rust_self_member_calls", frozenset({".rs"}), _resolve_rust_self_member_calls) ) +register_language_resolver( + LanguageResolver( + "solidity_type_references", frozenset({".sol"}), resolve_solidity_type_references + ) +) register_language_resolver( LanguageResolver( "elixir_import_targets", @@ -6279,6 +6289,7 @@ def add_existing_edge(edge: dict) -> None: ".cts": extract_js, ".go": extract_go, ".rs": extract_rust, + ".sol": extract_solidity, ".java": extract_java, ".groovy": extract_groovy, ".gradle": extract_groovy, @@ -6378,6 +6389,7 @@ def add_existing_edge(edge: dict) -> None: # rather than falling back like Pascal does. Used by the #1745 warning in # extract() to tell the user which extra restores the language. _EXTRA_FOR_EXTENSION = { + ".sol": "solidity", ".sql": "sql", ".tf": "terraform", ".tfvars": "terraform", diff --git a/graphify/extractors/__init__.py b/graphify/extractors/__init__.py index 68ff3340c3..24e18f0858 100644 --- a/graphify/extractors/__init__.py +++ b/graphify/extractors/__init__.py @@ -29,6 +29,7 @@ from graphify.extractors.razor import extract_razor from graphify.extractors.rust import extract_rust from graphify.extractors.sln import extract_sln +from graphify.extractors.solidity import extract_solidity from graphify.extractors.sql import extract_sql from graphify.extractors.terraform import extract_terraform from graphify.extractors.verilog import extract_verilog @@ -59,6 +60,7 @@ "razor": extract_razor, "rust": extract_rust, "sln": extract_sln, + "solidity": extract_solidity, "sql": extract_sql, "terraform": extract_terraform, "verilog": extract_verilog, diff --git a/graphify/extractors/solidity.py b/graphify/extractors/solidity.py new file mode 100644 index 0000000000..eb1852e4e5 --- /dev/null +++ b/graphify/extractors/solidity.py @@ -0,0 +1,408 @@ +"""Structural extraction for Solidity source files.""" +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from tree_sitter import Node + +from graphify.extractors.base import _file_stem, _make_id, _read_text + + +_TYPE_DECLARATIONS = { + "contract_declaration": "contract", + "interface_declaration": "interface", + "library_declaration": "library", +} + + +def _unquote(value: str) -> str: + value = value.strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in "'\"": + return value[1:-1] + return value + + +def resolve_solidity_type_references( + _per_file: list[dict], all_nodes: list[dict], all_edges: list[dict] +) -> None: + """Bind inheritance and using references to types in the file or its imports.""" + node_by_id = {node.get("id"): node for node in all_nodes} + file_id_by_source = { + str(node["source_file"]): node["id"] + for node in all_nodes + if node.get("source_file") + and node.get("label") == Path(str(node["source_file"])).name + } + source_by_file_id = {nid: source for source, nid in file_id_by_source.items()} + imported_sources: dict[str, set[str]] = {} + for edge in all_edges: + if edge.get("relation") != "imports_from": + continue + source_file = source_by_file_id.get(edge.get("source")) + target_file = source_by_file_id.get(edge.get("target")) + if source_file and target_file: + imported_sources.setdefault(source_file, set()).add(target_file) + + types: dict[tuple[str, str], list[str]] = {} + for node in all_nodes: + metadata = node.get("metadata") + if not isinstance(metadata, dict) or metadata.get("kind") not in _TYPE_DECLARATIONS.values(): + continue + source_file = node.get("source_file") + if source_file: + types.setdefault((str(source_file), str(node.get("label", ""))), []).append(node["id"]) + + for edge in all_edges: + context = edge.get("context") + if not isinstance(context, str) or not context.startswith("solidity_type:"): + continue + source_node = node_by_id.get(edge.get("source")) + if not source_node or not source_node.get("source_file"): + continue + source_file = str(source_node["source_file"]) + name = context.split(":", 1)[1] + candidates = list(types.get((source_file, name), [])) + for imported in imported_sources.get(source_file, set()): + candidates.extend(types.get((imported, name), [])) + if len(candidates) == 1: + edge["target"] = candidates[0] + + +def extract_solidity(path: Path) -> dict: + try: + import tree_sitter_solidity + from tree_sitter import Language, Parser + except ImportError: + return {"nodes": [], "edges": [], "error": "tree-sitter-solidity not installed"} + + try: + source = path.read_bytes() + root = Parser(Language(tree_sitter_solidity.language())).parse(source).root_node + except Exception as exc: + return {"nodes": [], "edges": [], "error": f"Solidity grammar failed to load: {exc}"} + + source_file = str(path) + stem = _file_stem(path) + file_id = _make_id(source_file) + nodes: list[dict[str, Any]] = [] + edges: list[dict[str, Any]] = [] + raw_calls: list[dict[str, Any]] = [] + seen_ids: set[str] = set() + seen_edges: set[tuple[str, str, str]] = set() + + def add_node( + nid: str, + label: str, + node: Node, + *, + kind: str, + source_backed: bool = True, + callable_node: bool = False, + ) -> str: + if nid not in seen_ids: + seen_ids.add(nid) + item: dict[str, Any] = { + "id": nid, + "label": label, + "file_type": "code", + "source_location": f"L{node.start_point[0] + 1}", + "metadata": {"language": "solidity", "kind": kind}, + } + if source_backed: + item["source_file"] = source_file + if callable_node: + item["_callable"] = True + nodes.append(item) + return nid + + def add_edge( + source_id: str, + target_id: str, + relation: str, + node: Node, + *, + context: str | None = None, + target_file: str | None = None, + ) -> None: + key = (source_id, target_id, relation) + if not source_id or not target_id or source_id == target_id or key in seen_edges: + return + seen_edges.add(key) + item: dict[str, Any] = { + "source": source_id, + "target": target_id, + "relation": relation, + "confidence": "EXTRACTED", + "source_file": source_file, + "source_location": f"L{node.start_point[0] + 1}", + "weight": 1.0, + } + if context: + item["context"] = context + if target_file: + item["target_file"] = target_file + edges.append(item) + + add_node(file_id, path.name, root, kind="file") + + def type_stub(name: str, node: Node) -> str: + name = name.split(".")[-1] + nid = _make_id("solidity", "type", name) + add_node(nid, name, node, kind="external_type", source_backed=False) + return nid + + for declaration in root.named_children: + if declaration.type != "import_directive": + continue + source_node = declaration.child_by_field_name("source") + if source_node is None: + continue + relative = _unquote(_read_text(source_node, source)) + if not relative or relative.startswith(("http:", "https:")): + continue + target = path.parent / relative + add_edge( + file_id, + _make_id(str(target)), + "imports_from", + declaration, + target_file=str(target), + ) + + for declaration in root.named_children: + type_kind = _TYPE_DECLARATIONS.get(declaration.type) + if type_kind is None: + continue + name_node = declaration.child_by_field_name("name") + body = declaration.child_by_field_name("body") + if name_node is None or body is None: + continue + type_name = _read_text(name_node, source) + type_id = add_node( + _make_id(stem, type_kind, type_name), + type_name, + declaration, + kind=type_kind, + callable_node=type_kind == "contract", + ) + add_edge(file_id, type_id, "contains", declaration) + + for child in declaration.named_children: + if child.type != "inheritance_specifier": + continue + ancestor = child.child_by_field_name("ancestor") + if ancestor is None: + continue + base_name = _read_text(ancestor, source).split(".")[-1] + add_edge( + type_id, + type_stub(base_name, child), + "inherits", + child, + context=f"solidity_type:{base_name}", + ) + + functions: dict[tuple[str, int], list[str]] = {} + modifiers: dict[str, str] = {} + reference_targets: dict[tuple[str, int], str] = {} + bodies: list[tuple[Node, str]] = [] + pending_modifiers: list[tuple[str, str, Node]] = [] + + def add_member( + member: Node, + name: str, + label: str, + kind: str, + *, + relation: str = "contains", + signature: str = "", + callable_node: bool = False, + ) -> str: + member_id = _make_id(type_id, kind, name, signature or str(member.start_point[0])) + add_node( + member_id, + label, + member, + kind=kind, + callable_node=callable_node, + ) + add_edge(type_id, member_id, relation, member) + return member_id + + for member in body.named_children: + kind = member.type + if kind == "using_directive": + alias = next( + ( + _read_text(child, source) + for child in member.named_children + if child.type in {"type_alias", "identifier", "user_defined_type"} + ), + "", + ) + if alias: + name = alias.split(".")[-1] + add_edge( + type_id, + type_stub(name, member), + "uses", + member, + context=f"solidity_type:{name}", + ) + continue + + if kind == "struct_declaration": + member_name_node = member.child_by_field_name("name") + struct_body = member.child_by_field_name("body") + if member_name_node is None: + continue + name = _read_text(member_name_node, source) + struct_id = add_member(member, name, name, "struct") + if struct_body is not None: + for field in struct_body.named_children: + if field.type != "struct_member": + continue + field_name_node = field.child_by_field_name("name") + if field_name_node is None: + continue + field_name = _read_text(field_name_node, source) + field_id = _make_id(struct_id, "field", field_name) + add_node(field_id, field_name, field, kind="field") + add_edge(struct_id, field_id, "contains", field) + continue + + if kind == "enum_declaration": + member_name_node = member.child_by_field_name("name") + enum_body = member.child_by_field_name("body") + if member_name_node is None: + continue + name = _read_text(member_name_node, source) + enum_id = add_member(member, name, name, "enum") + if enum_body is not None: + for value in enum_body.named_children: + if value.type != "enum_value": + continue + value_name = _read_text(value, source) + value_id = _make_id(enum_id, value_name) + add_node(value_id, value_name, value, kind="enum_value") + add_edge(enum_id, value_id, "contains", value) + continue + + simple_kinds = { + "event_definition": "event", + "error_declaration": "error", + "state_variable_declaration": "state_variable", + } + if kind in simple_kinds: + member_name_node = member.child_by_field_name("name") + if member_name_node is None: + continue + name = _read_text(member_name_node, source) + member_id = add_member(member, name, name, simple_kinds[kind]) + arity = sum(child.type in {"event_parameter", "parameter"} for child in member.named_children) + reference_targets[(name, arity)] = member_id + continue + + if kind not in { + "function_definition", "constructor_definition", "modifier_definition", + "fallback_receive_definition", + }: + continue + + name_node = member.child_by_field_name("name") + if kind == "constructor_definition": + name = "constructor" + callable_kind = "constructor" + elif kind == "fallback_receive_definition": + text = _read_text(member, source).lstrip() + name = "receive" if text.startswith("receive") else "fallback" + callable_kind = name + else: + if name_node is None: + continue + name = _read_text(name_node, source) + callable_kind = "modifier" if kind == "modifier_definition" else "function" + + parameters = [ + child for child in member.named_children + if child.type in {"parameter", "fallback_receive_parameter"} + ] + signature = ",".join( + _read_text(child.child_by_field_name("type"), source) + for child in parameters + if child.child_by_field_name("type") is not None + ) + arity = len(parameters) + member_id = add_member( + member, + name, + f"{name}()", + callable_kind, + relation="method", + signature=f"{arity}:{signature}", + callable_node=True, + ) + if callable_kind == "modifier": + modifiers[name] = member_id + else: + functions.setdefault((name, arity), []).append(member_id) + member_body = member.child_by_field_name("body") + if member_body is not None: + bodies.append((member_body, member_id)) + if callable_kind == "function": + for child in member.named_children: + if child.type != "modifier_invocation": + continue + modifier_name = next( + ( + _read_text(item, source) + for item in child.named_children + if item.type == "identifier" + ), + "", + ) + if modifier_name: + pending_modifiers.append((member_id, modifier_name, child)) + + for caller_id, modifier_name, modifier_node in pending_modifiers: + target = modifiers.get(modifier_name) + if target: + add_edge(caller_id, target, "uses", modifier_node) + + for function_body, caller_id in bodies: + stack = [function_body] + while stack: + node = stack.pop() + if node.type == "call_expression": + callee_node = node.child_by_field_name("function") + if callee_node is not None: + written = _read_text(callee_node, source) + callee = written.split(".")[-1] + arity = sum(child.type == "call_argument" for child in node.named_children) + candidates: list[str] = [] + if "." not in written or written.startswith("this."): + candidates = functions.get((callee, arity), []) + if len(candidates) == 1: + add_edge(caller_id, candidates[0], "calls", node) + else: + referenced = reference_targets.get((callee, arity)) + if referenced is not None and "." not in written: + add_edge(caller_id, referenced, "references", node) + elif callee and "." in written: + raw_calls.append({ + "caller_nid": caller_id, + "callee": callee, + "is_member_call": True, + "language": "solidity", + "source_file": source_file, + "source_location": f"L{node.start_point[0] + 1}", + }) + stack.extend(reversed(node.named_children)) + + clean_edges = [ + edge for edge in edges + if edge["source"] in seen_ids + and (edge["target"] in seen_ids or edge["relation"] == "imports_from") + ] + return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls} diff --git a/pyproject.toml b/pyproject.toml index 5e39d8c54f..f07e42a140 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,8 @@ commonlisp = ["tree-sitter-commonlisp"] # every platform, no C toolchain; optional because Robot Framework corpora are # QA-automation specific. robot = ["robotframework>=4.0"] -all = ["mcp>=1,<3", "starlette>=1.3.1,<2", "neo4j", "falkordb", "pypdf>=6.16.1", "markdownify", "watchdog", "graspologic; python_version < '3.13'", "graspologic-native>=1.3.1,<2; python_version >= '3.13'", "python-docx", "openpyxl", "psycopg[binary]", "faster-whisper; python_version >= '3.11'", "yt-dlp>=2026.7.4", "matplotlib", "pillow>=12.3.0", "numpy>=2.0; python_version >= '3.13'", "openai", "tiktoken", "boto3", "anthropic", "tree-sitter-sql", "jieba; python_version < '3.12'", "jieba-py>=0.46.12,<1; python_version >= '3.12'", "tree-sitter-dm", "tree-sitter-hcl", "tree-sitter-pascal", "tree-sitter-ocaml", "tree-sitter-commonlisp", "robotframework>=4.0"] +solidity = ["tree-sitter-solidity==1.2.13"] +all = ["mcp>=1,<3", "starlette>=1.3.1,<2", "neo4j", "falkordb", "pypdf>=6.16.1", "markdownify", "watchdog", "graspologic; python_version < '3.13'", "graspologic-native>=1.3.1,<2; python_version >= '3.13'", "python-docx", "openpyxl", "psycopg[binary]", "faster-whisper; python_version >= '3.11'", "yt-dlp>=2026.7.4", "matplotlib", "pillow>=12.3.0", "numpy>=2.0; python_version >= '3.13'", "openai", "tiktoken", "boto3", "anthropic", "tree-sitter-sql", "jieba; python_version < '3.12'", "jieba-py>=0.46.12,<1; python_version >= '3.12'", "tree-sitter-dm", "tree-sitter-hcl", "tree-sitter-pascal", "tree-sitter-ocaml", "tree-sitter-commonlisp", "robotframework>=4.0", "tree-sitter-solidity==1.2.13"] [project.scripts] graphify = "graphify.__main__:main" diff --git a/tests/fixtures/new_languages/sample.sol b/tests/fixtures/new_languages/sample.sol new file mode 100644 index 0000000000..b1d14e5e9d --- /dev/null +++ b/tests/fixtures/new_languages/sample.sol @@ -0,0 +1,9 @@ +pragma solidity ^0.8.20; + +contract Sample { + function run() public { + helper(); + } + + function helper() internal {} +} diff --git a/tests/test_solidity_extractor.py b/tests/test_solidity_extractor.py new file mode 100644 index 0000000000..979e1e59b0 --- /dev/null +++ b/tests/test_solidity_extractor.py @@ -0,0 +1,130 @@ +"""Extraction coverage for solidity.""" + + +from __future__ import annotations + + + + + +import sys + + +from pathlib import Path + + + + + +from graphify.extract import extract + + + + + +FIXTURE = Path(__file__).parent / "fixtures" / "new_languages" / "sample.sol" + + + + + +def _edge_labels(result: dict, relation: str) -> set[tuple[str, str]]: + labels = {node["id"]: node["label"] for node in result["nodes"]} + return { + (labels.get(edge["source"], edge["source"]), labels.get(edge["target"], edge["target"])) + for edge in result["edges"] + if edge["relation"] == relation + } + + +def test_solidity_contract_members_and_calls_are_extracted(tmp_path): + source = tmp_path / "Counter.sol" + source.write_text( + "pragma solidity ^0.8.0;\n" + "contract Counter {\n" + " uint public count;\n" + " event Counted(uint value);\n" + " function increment() public { record(); }\n" + " function record() internal {}\n" + "}\n", + encoding="utf-8", + ) + + result = extract([source], cache_root=tmp_path) + + labels = {node["label"] for node in result["nodes"]} + assert {"Counter", "increment()", "record()", "Counted"} <= labels + assert ("increment()", "record()") in _edge_labels(result, "calls") + + +def test_solidity_types_imports_inheritance_overloads_and_modifiers(tmp_path): + (tmp_path / "Base.sol").write_text( + "contract Base { function baseRun() internal {} }\n", encoding="utf-8" + ) + (tmp_path / "Lib.sol").write_text( + "library Lib { function touch(uint value) internal {} }\n", encoding="utf-8" + ) + source = tmp_path / "Counter.sol" + source.write_text( + "pragma solidity ^0.8.20;\n" + 'import "./Base.sol";\n' + 'import "./Lib.sol";\n' + "contract Counter is Base {\n" + " using Lib for uint;\n" + " struct Item { uint value; }\n" + " enum State { On, Off }\n" + " event Changed(uint value);\n" + " error Failed();\n" + " uint public count;\n" + " modifier guarded() { _; }\n" + " constructor() {}\n" + " function run() public guarded { record(); this.record(1); }\n" + " function record() internal {}\n" + " function record(uint value) internal {}\n" + "}\n", + encoding="utf-8", + ) + + result = extract( + [source, tmp_path / "Base.sol", tmp_path / "Lib.sol"], cache_root=tmp_path + ) + + labels = [node["label"] for node in result["nodes"]] + assert { + "Counter", "Base", "Lib", "Item", "value", "State", "On", "Off", + "Changed", "Failed", "count", "guarded()", "constructor()", "run()", + } <= set(labels) + assert labels.count("record()") == 2 + assert ("Counter", "Base") in _edge_labels(result, "inherits") + assert ("run()", "guarded()") in _edge_labels(result, "uses") + assert len([edge for edge in result["edges"] if edge["relation"] == "imports_from"]) == 2 + + +def test_solidity_fixture_uses_normal_extract_path(tmp_path): + result = extract([FIXTURE], cache_root=tmp_path) + + labels = {node["label"] for node in result["nodes"]} + assert {'run()', 'helper()', 'Sample'} <= labels + assert ('run()', 'helper()') in _edge_labels(result, "calls") + + +def test_solidity_malformed_tail_comments_and_strings_do_not_create_phantoms(tmp_path): + source = tmp_path / 'Broken.sol' + source.write_text('contract Kept { function valid() public {} string constant text = "function Ghost()"; /* function Hidden() {} */', encoding="utf-8") + + result = extract([source], cache_root=tmp_path) + + labels = {node["label"].casefold() for node in result["nodes"]} + assert 'valid()' in labels + assert labels.isdisjoint({'ghost()', 'hidden()'}) + + +def test_solidity_missing_parser_reports_install_hint(tmp_path, monkeypatch, capsys): + source = tmp_path / "missing.sol" + source.write_text('contract Missing {}\n', encoding="utf-8") + monkeypatch.setitem(sys.modules, 'tree_sitter_solidity', None) + + result = extract([source], cache_root=tmp_path) + + assert result["nodes"] == [] + assert 'pip install "graphifyy[solidity]"' in capsys.readouterr().err diff --git a/uv.lock b/uv.lock index fd04b1bea5..5696118099 100644 --- a/uv.lock +++ b/uv.lock @@ -1162,6 +1162,7 @@ all = [ { name = "tree-sitter-hcl" }, { name = "tree-sitter-ocaml" }, { name = "tree-sitter-pascal" }, + { name = "tree-sitter-solidity" }, { name = "tree-sitter-sql" }, { name = "watchdog" }, { name = "yt-dlp" }, @@ -1235,6 +1236,9 @@ postgres = [ robot = [ { name = "robotframework" }, ] +solidity = [ + { name = "tree-sitter-solidity" }, +] sql = [ { name = "tree-sitter-sql" }, ] @@ -1363,6 +1367,8 @@ requires-dist = [ { name = "tree-sitter-ruby", specifier = ">=0.23,<0.25" }, { name = "tree-sitter-rust", specifier = ">=0.23,<0.25" }, { name = "tree-sitter-scala", specifier = ">=0.23,<0.27" }, + { name = "tree-sitter-solidity", marker = "extra == 'all'", specifier = "==1.2.13" }, + { name = "tree-sitter-solidity", marker = "extra == 'solidity'", specifier = "==1.2.13" }, { name = "tree-sitter-sql", marker = "extra == 'all'" }, { name = "tree-sitter-sql", marker = "extra == 'postgres'" }, { name = "tree-sitter-sql", marker = "extra == 'sql'" }, @@ -1375,7 +1381,7 @@ requires-dist = [ { name = "yt-dlp", marker = "extra == 'all'", specifier = ">=2026.7.4" }, { name = "yt-dlp", marker = "extra == 'video'", specifier = ">=2026.7.4" }, ] -provides-extras = ["mcp", "neo4j", "falkordb", "pdf", "watch", "svg", "leiden", "office", "google", "postgres", "video", "kimi", "ollama", "bedrock", "anthropic", "gemini", "openai", "chinese", "sql", "pascal", "dm", "terraform", "ocaml", "commonlisp", "robot", "all"] +provides-extras = ["mcp", "neo4j", "falkordb", "pdf", "watch", "svg", "leiden", "office", "google", "postgres", "video", "kimi", "ollama", "bedrock", "anthropic", "gemini", "openai", "chinese", "sql", "pascal", "dm", "terraform", "ocaml", "commonlisp", "robot", "solidity", "all"] [package.metadata.requires-dev] dev = [ @@ -4995,6 +5001,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/9a/578b52f4f94d50352ac04630c46d49966b8564bd424cf270ed016c86bc72/tree_sitter_scala-0.26.0-cp39-abi3-win_arm64.whl", hash = "sha256:0eb627916fd1448657b4bcbe178e0cab8d3c114ec04aec51f0d0cd5ca2aa996e", size = 608073, upload-time = "2026-04-18T22:23:57.855Z" }, ] +[[package]] +name = "tree-sitter-solidity" +version = "1.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/6e/c40fcf4018302b110d48e02e2fd8128e9a009669cae04ba8e3c849e293fb/tree_sitter_solidity-1.2.13.tar.gz", hash = "sha256:5de530ee9a3d18632396c55cd51f87fd4a7dae8ebac117eb63e52b4d3bc4a7c1", size = 158785, upload-time = "2025-08-04T11:14:26.576Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/cf/9387cd5cda028790dada6f999625e19489d7d76e8e92999403421ad7b1ce/tree_sitter_solidity-1.2.13-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:a662d040cbe9d1a9f1083eb2be461c60748b7c90782de1f7505bf6494a1d5c05", size = 59074, upload-time = "2025-08-04T11:14:19.404Z" }, + { url = "https://files.pythonhosted.org/packages/20/35/a8a5443d95333984914ef51c661bd85b20fee2374b0b866243d25bbc0efe/tree_sitter_solidity-1.2.13-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:703fa18a90289e20cec25ddfc0fc574440e86781e3c5d26c99acb757a6acb109", size = 61855, upload-time = "2025-08-04T11:14:20.244Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c7/c329e23b3b93c3d55d887e0320794ea42681770704987468060964608cfb/tree_sitter_solidity-1.2.13-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fd017ffcdcf38422175176d1b3623a3499025c6c7c896b2502c0bd55bd2b36d", size = 94185, upload-time = "2025-08-04T11:14:21.31Z" }, + { url = "https://files.pythonhosted.org/packages/de/81/685a3c5dec31ba2c38615da84b74bc8973a77a0989091c5e2efaa7f5e4e8/tree_sitter_solidity-1.2.13-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:490a11968c79a17f9b01156d2d655f3e602ff8de9256b026cf1deb6213d34c78", size = 91941, upload-time = "2025-08-04T11:14:22.32Z" }, + { url = "https://files.pythonhosted.org/packages/94/38/3b185a4f17d53144892ff275bbb79ad7c3f27c6227b4d5b3e8dd01007477/tree_sitter_solidity-1.2.13-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6e008987fd7e2329b14b2558ae16b74e98460d2ac50a98affa8d9951772c977e", size = 90993, upload-time = "2025-08-04T11:14:23.536Z" }, + { url = "https://files.pythonhosted.org/packages/57/73/f546c9f85cea28da4bb8abb4c0f4efb05694497a5c7d5a222dce6451abc4/tree_sitter_solidity-1.2.13-cp38-abi3-win_amd64.whl", hash = "sha256:3f700a305167e39b984d38dceddf45a56e898124a923bf5cb6bb4b149c2c7063", size = 69156, upload-time = "2025-08-04T11:14:24.608Z" }, + { url = "https://files.pythonhosted.org/packages/55/37/316728600710cb8f24dbbcc6f1471b808d722285a65badb7dfc7c500fa94/tree_sitter_solidity-1.2.13-cp38-abi3-win_arm64.whl", hash = "sha256:5caa0b2eafdb5e27e3f035732a6ccca5758231befb08f6adc4287b5e118ab8d2", size = 61063, upload-time = "2025-08-04T11:14:25.595Z" }, +] + [[package]] name = "tree-sitter-sql" version = "0.3.11"