From 3a92853329abede2446b217ebf1b4e635e5e7ff9 Mon Sep 17 00:00:00 2001 From: Abdullah Date: Mon, 21 Sep 2026 16:25:48 +0530 Subject: [PATCH] feat: add Erlang language extractor --- graphify/analyze.py | 1 + graphify/detect.py | 2 +- graphify/extract.py | 15 ++ graphify/extractors/__init__.py | 2 + graphify/extractors/erlang.py | 333 ++++++++++++++++++++++++ pyproject.toml | 4 +- tests/fixtures/new_languages/sample.erl | 5 + tests/test_erlang_extractor.py | 124 +++++++++ uv.lock | 58 ++++- 9 files changed, 541 insertions(+), 3 deletions(-) create mode 100644 graphify/extractors/erlang.py create mode 100644 tests/fixtures/new_languages/sample.erl create mode 100644 tests/test_erlang_extractor.py diff --git a/graphify/analyze.py b/graphify/analyze.py index df1c20661..78ce8774f 100644 --- a/graphify/analyze.py +++ b/graphify/analyze.py @@ -41,6 +41,7 @@ _LANG_FAMILY: dict[str, str] = { **{e: "dotnet" for e in (".cs",)}, **{e: "php" for e in (".php",)}, **{e: "r" for e in (".r",)}, + **{e: "erlang" for e in (".erl", ".hrl", ".escript")}, } diff --git a/graphify/detect.py b/graphify/detect.py index eec8aaf1d..2a213dc4d 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -41,7 +41,7 @@ _MANIFEST_PATH = str(out_path("manifest.json")) _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', '.erl', '.hrl', '.escript', '.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 da66f4131..3d19f836a 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -45,6 +45,7 @@ from graphify.extractors.csharp import ( from graphify.extractors.dart import extract_dart # noqa: F401 from graphify.extractors.dm import extract_dm, extract_dmf, extract_dmi, extract_dmm # noqa: F401 from graphify.extractors.elixir import extract_elixir # noqa: F401 +from graphify.extractors.erlang import extract_erlang, resolve_erlang_remote_calls # noqa: F401 from graphify.extractors.fortran import _cpp_preprocess, extract_fortran # noqa: F401 from graphify.extractors.go import _GO_PREDECLARED_FUNCS, extract_go # noqa: F401 from graphify.extractors.json_config import extract_json # noqa: F401 @@ -2763,6 +2764,7 @@ _LANG_FAMILY_BY_EXT: dict[str, str] = { ".py": "python", ".go": "go", ".rs": "rust", + ".erl": "erlang", ".hrl": "erlang", ".escript": "erlang", ".rb": "ruby", ".rake": "ruby", ".php": "php", ".phtml": "php", ".php3": "php", ".php4": "php", ".php5": "php", ".php7": "php", ".phps": "php", @@ -5194,6 +5196,13 @@ register_language_resolver( register_language_resolver( LanguageResolver("rust_self_member_calls", frozenset({".rs"}), _resolve_rust_self_member_calls) ) +register_language_resolver( + LanguageResolver( + "erlang_remote_calls", + frozenset({".erl", ".hrl", ".escript"}), + resolve_erlang_remote_calls, + ) +) register_language_resolver( LanguageResolver( "elixir_import_targets", @@ -6307,6 +6316,9 @@ _DISPATCH: dict[str, Any] = { ".psd1": extract_powershell_manifest, ".ex": extract_elixir, ".exs": extract_elixir, + ".erl": extract_erlang, + ".hrl": extract_erlang, + ".escript": extract_erlang, ".m": extract_objc, ".mm": extract_objc, ".jl": extract_julia, @@ -6378,6 +6390,9 @@ _DISPATCH: dict[str, Any] = { # 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 = { + ".erl": "erlang", + ".hrl": "erlang", + ".escript": "erlang", ".sql": "sql", ".tf": "terraform", ".tfvars": "terraform", diff --git a/graphify/extractors/__init__.py b/graphify/extractors/__init__.py index 68ff3340c..67bfa558f 100644 --- a/graphify/extractors/__init__.py +++ b/graphify/extractors/__init__.py @@ -17,6 +17,7 @@ from graphify.extractors.commonlisp import extract_commonlisp from graphify.extractors.dart import extract_dart from graphify.extractors.dm import extract_dm, extract_dmf, extract_dmi, extract_dmm from graphify.extractors.elixir import extract_elixir +from graphify.extractors.erlang import extract_erlang from graphify.extractors.fortran import extract_fortran from graphify.extractors.go import extract_go from graphify.extractors.json_config import extract_json @@ -46,6 +47,7 @@ LANGUAGE_EXTRACTORS: dict[str, Callable[[Path], dict]] = { "dmi": extract_dmi, "dmm": extract_dmm, "elixir": extract_elixir, + "erlang": extract_erlang, "fortran": extract_fortran, "go": extract_go, "json": extract_json, diff --git a/graphify/extractors/erlang.py b/graphify/extractors/erlang.py new file mode 100644 index 000000000..4df8a686c --- /dev/null +++ b/graphify/extractors/erlang.py @@ -0,0 +1,333 @@ +"""Structural extraction for Erlang source and header files.""" +from __future__ import annotations + +from pathlib import Path +from typing import Any, Iterable + +from tree_sitter import Node + +from graphify.extractors.base import _file_stem, _make_id, _read_text + + +def _atom(value: str) -> str: + value = value.strip() + if len(value) >= 2 and value[0] == value[-1] == "'": + return value[1:-1] + return value + + +def _descendants(node: Node) -> Iterable[Node]: + for child in node.named_children: + yield child + yield from _descendants(child) + + +def resolve_erlang_remote_calls( + per_file: list[dict], all_nodes: list[dict], all_edges: list[dict] +) -> None: + """Resolve module-qualified calls by exact module, name, and arity.""" + functions: dict[tuple[str, str, int], list[str]] = {} + for node in all_nodes: + metadata = node.get("metadata") + if not isinstance(metadata, dict) or metadata.get("language") != "erlang": + continue + if metadata.get("kind") != "function": + continue + module = metadata.get("module") + name = metadata.get("name") + arity = metadata.get("arity") + if isinstance(module, str) and isinstance(name, str) and isinstance(arity, int): + functions.setdefault((module, name, arity), []).append(node["id"]) + + existing = { + (edge.get("source"), edge.get("target")) + for edge in all_edges + if edge.get("relation") == "calls" + } + for result in per_file: + for call in result.get("raw_calls", []): + if call.get("language") != "erlang" or not call.get("remote_module"): + continue + key = ( + str(call["remote_module"]), + str(call.get("callee", "")), + int(call.get("arity", -1)), + ) + candidates = functions.get(key, []) + caller = call.get("caller_nid") + if len(candidates) != 1 or candidates[0] == caller: + continue + pair = (caller, candidates[0]) + if pair in existing: + continue + existing.add(pair) + all_edges.append({ + "source": caller, + "target": candidates[0], + "relation": "calls", + "context": "remote_call", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": call.get("source_file", ""), + "source_location": call.get("source_location"), + "weight": 1.0, + }) + + +def extract_erlang(path: Path) -> dict: + try: + from tree_sitter import Parser + from tree_sitter_language_pack import get_language + except ImportError: + return {"nodes": [], "edges": [], "error": "tree-sitter-language-pack not installed"} + + try: + source = path.read_bytes() + root = Parser(get_language("erlang")).parse(source).root_node + except Exception as exc: + return {"nodes": [], "edges": [], "error": f"Erlang 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, + metadata: dict[str, Any] | None = None, + ) -> str: + if nid not in seen_ids: + seen_ids.add(nid) + details: dict[str, Any] = {"language": "erlang", "kind": kind} + if metadata: + details.update(metadata) + item: dict[str, Any] = { + "id": nid, + "label": label, + "file_type": "code", + "source_location": f"L{node.start_point[0] + 1}", + "metadata": details, + } + 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) -> 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) + edges.append({ + "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, + }) + + add_node(file_id, path.name, root, kind="file") + + module_name = path.stem + module_declaration = next( + (node for node in root.named_children if node.type == "module_attribute"), + None, + ) + module_id = file_id + if module_declaration is not None: + name_node = module_declaration.child_by_field_name("name") + if name_node is not None: + module_name = _atom(_read_text(name_node, source)) + module_id = add_node( + _make_id(stem, "module", module_name), + module_name, + module_declaration, + kind="module", + metadata={"module": module_name}, + ) + add_edge(file_id, module_id, "contains", module_declaration) + + functions: dict[tuple[str, int], str] = {} + bodies: list[tuple[Node, str]] = [] + export_specs: list[tuple[str, int, Node]] = [] + + for node in root.named_children: + if node.type == "export_attribute": + for fa in (item for item in _descendants(node) if item.type == "fa"): + atom_node = next((item for item in _descendants(fa) if item.type == "atom"), None) + integer = next((item for item in _descendants(fa) if item.type == "integer"), None) + if atom_node is not None and integer is not None: + export_specs.append(( + _atom(_read_text(atom_node, source)), + int(_read_text(integer, source)), + fa, + )) + continue + + if node.type == "behaviour_attribute": + name_node = node.child_by_field_name("name") + if name_node is not None: + name = _atom(_read_text(name_node, source)) + target = add_node( + _make_id("erlang", "behaviour", name), + name, + name_node, + kind="behaviour", + source_backed=False, + ) + add_edge(module_id, target, "implements", node) + continue + + if node.type in {"pp_include", "pp_include_lib"}: + string_node = next( + (item for item in node.named_children if item.type == "string"), None + ) + if string_node is None: + continue + include = _read_text(string_node, source).strip("\"") + if node.type == "pp_include": + target = path.parent / include + target_id = _make_id(str(target)) + edge = { + "source": module_id, + "target": target_id, + "relation": "imports_from", + "confidence": "EXTRACTED", + "source_file": source_file, + "source_location": f"L{node.start_point[0] + 1}", + "weight": 1.0, + "target_file": str(target), + } + key = (module_id, target_id, "imports_from") + if key not in seen_edges: + seen_edges.add(key) + edges.append(edge) + else: + target = add_node( + _make_id("erlang", "include_lib", include), + include, + string_node, + kind="include_lib", + source_backed=False, + ) + add_edge(module_id, target, "imports", node) + continue + + declaration_kinds = { + "record_decl": ("record", "name"), + "type_alias": ("type", "name"), + "opaque": ("type", "name"), + "pp_define": ("macro", "lhs"), + } + if node.type in declaration_kinds: + kind, field = declaration_kinds[node.type] + name_container = node.child_by_field_name(field) + if name_container is None: + continue + if kind == "type": + name_node = name_container.child_by_field_name("name") + name = _atom(_read_text(name_node or name_container, source)) + args = name_container.child_by_field_name("args") + arity = len(args.named_children) if args is not None else 0 + label = f"{name}/{arity}" + elif kind == "macro": + name_node = name_container.child_by_field_name("name") + name = _read_text(name_node or name_container, source) + label = name + else: + name = _atom(_read_text(name_container, source)) + label = name + declaration_id = add_node( + _make_id(module_id, kind, name), + label, + node, + kind=kind, + metadata={"module": module_name, "name": name}, + ) + add_edge(module_id, declaration_id, "contains", node) + continue + + if node.type != "fun_decl": + continue + clauses = [ + child for child in node.named_children if child.type == "function_clause" + ] + for clause in clauses: + name_node = clause.child_by_field_name("name") + args = clause.child_by_field_name("args") + if name_node is None or args is None: + continue + name = _atom(_read_text(name_node, source)) + arity = len(args.named_children) + key = (name, arity) + function_id = functions.get(key) + if function_id is None: + function_id = add_node( + _make_id(module_id, "function", name, str(arity)), + f"{name}/{arity}", + clause, + kind="function", + callable_node=True, + metadata={"module": module_name, "name": name, "arity": arity}, + ) + functions[key] = function_id + add_edge(module_id, function_id, "contains", clause) + body = clause.child_by_field_name("body") + if body is not None: + bodies.append((body, function_id)) + + for name, arity, spec in export_specs: + target = functions.get((name, arity)) + if target is not None: + add_edge(module_id, target, "exports", spec) + + for body, caller_id in bodies: + stack = [body] + while stack: + node = stack.pop() + if node.type == "call": + callee_node = node.child_by_field_name("expr") + args = node.child_by_field_name("args") + if callee_node is not None and args is not None: + arity = len(args.named_children) + if callee_node.type == "atom": + name = _atom(_read_text(callee_node, source)) + target = functions.get((name, arity)) + if target is not None: + add_edge(caller_id, target, "calls", node) + elif callee_node.type == "remote": + written = _read_text(callee_node, source) + module, separator, name = written.partition(":") + if separator: + raw_calls.append({ + "caller_nid": caller_id, + "callee": _atom(name), + "arity": arity, + "remote_module": _atom(module), + "is_member_call": True, + "language": "erlang", + "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 5e39d8c54..99f63c796 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,9 @@ 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"] +# The language pack bundles the Erlang grammar in an offline-compatible wheel. +erlang = ["tree-sitter-language-pack==0.11.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", "tree-sitter-language-pack==0.11.0"] [project.scripts] graphify = "graphify.__main__:main" diff --git a/tests/fixtures/new_languages/sample.erl b/tests/fixtures/new_languages/sample.erl new file mode 100644 index 000000000..3d0a7bb86 --- /dev/null +++ b/tests/fixtures/new_languages/sample.erl @@ -0,0 +1,5 @@ +-module(sample). +-export([run/0]). + +run() -> helper(). +helper() -> ok. diff --git a/tests/test_erlang_extractor.py b/tests/test_erlang_extractor.py new file mode 100644 index 000000000..dbc0dd59d --- /dev/null +++ b/tests/test_erlang_extractor.py @@ -0,0 +1,124 @@ +"""Extraction coverage for erlang.""" + + +from __future__ import annotations + + + + + +import sys + + +from pathlib import Path + + + + + +from graphify.extract import extract + + + + + +FIXTURE = Path(__file__).parent / "fixtures" / "new_languages" / "sample.erl" + + + + + +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_erlang_functions_resolve_by_name_and_arity(tmp_path): + source = tmp_path / "worker.erl" + source.write_text( + "-module(worker).\n" + "-export([run/1]).\n" + "run(X) -> helper(X).\n" + "helper(X) -> X.\n" + "helper(X, Y) -> X + Y.\n", + encoding="utf-8", + ) + + result = extract([source], cache_root=tmp_path) + + labels = {node["label"] for node in result["nodes"]} + assert {"worker", "run/1", "helper/1", "helper/2"} <= labels + assert ("run/1", "helper/1") in _edge_labels(result, "calls") + assert ("run/1", "helper/2") not in _edge_labels(result, "calls") + + +def test_erlang_attributes_includes_and_remote_calls(tmp_path): + header = tmp_path / "worker.hrl" + header.write_text("-define(HEADER_VALUE, 2).\n", encoding="utf-8") + other = tmp_path / "other.erl" + other.write_text( + "-module(other).\n-export([go/1]).\ngo(X) -> X.\n", encoding="utf-8" + ) + source = tmp_path / "worker.escript" + source.write_text( + "#!/usr/bin/env escript\n" + "-module('worker').\n" + "-behaviour(gen_server).\n" + '-include("worker.hrl").\n' + '-include_lib("stdlib/include/assert.hrl").\n' + "-export([run/1, 'quoted'/0]).\n" + "-record(state, {value}).\n" + "-type result() :: ok | error.\n" + "-define(DEFAULT, 1).\n" + "run(X) -> helper(X), other:go(X), io:format(\"x\").\n" + "helper(X) -> X.\n" + "'quoted'() -> ok.\n", + encoding="utf-8", + ) + + result = extract([source, other, header], cache_root=tmp_path) + + labels = {node["label"] for node in result["nodes"]} + assert { + "worker", "other", "run/1", "helper/1", "quoted/0", "go/1", + "state", "result/0", "DEFAULT", "HEADER_VALUE", "gen_server", + } <= labels + assert ("run/1", "helper/1") in _edge_labels(result, "calls") + assert ("run/1", "go/1") in _edge_labels(result, "calls") + assert ("worker", "run/1") in _edge_labels(result, "exports") + assert ("worker", "gen_server") in _edge_labels(result, "implements") + assert any(edge["relation"] == "imports_from" for edge in result["edges"]) + + +def test_erlang_fixture_uses_normal_extract_path(tmp_path): + result = extract([FIXTURE], cache_root=tmp_path) + + labels = {node["label"] for node in result["nodes"]} + assert {'run/0', 'helper/0', 'sample'} <= labels + assert ('run/0', 'helper/0') in _edge_labels(result, "calls") + + +def test_erlang_malformed_tail_comments_and_strings_do_not_create_phantoms(tmp_path): + source = tmp_path / 'broken.erl' + source.write_text('-module(broken).\nvalid() -> ok.\n% ghost() -> ok.\ninvalid(\n', encoding="utf-8") + + result = extract([source], cache_root=tmp_path) + + labels = {node["label"].casefold() for node in result["nodes"]} + assert 'valid/0' in labels + assert labels.isdisjoint({'ghost/0'}) + + +def test_erlang_missing_parser_reports_install_hint(tmp_path, monkeypatch, capsys): + source = tmp_path / "missing.erl" + source.write_text('-module(missing).\n', encoding="utf-8") + monkeypatch.setitem(sys.modules, 'tree_sitter_language_pack', None) + + result = extract([source], cache_root=tmp_path) + + assert result["nodes"] == [] + assert 'pip install "graphifyy[erlang]"' in capsys.readouterr().err diff --git a/uv.lock b/uv.lock index fd04b1bea..fdf1941b0 100644 --- a/uv.lock +++ b/uv.lock @@ -1160,6 +1160,7 @@ all = [ { name = "tree-sitter-commonlisp" }, { name = "tree-sitter-dm" }, { name = "tree-sitter-hcl" }, + { name = "tree-sitter-language-pack" }, { name = "tree-sitter-ocaml" }, { name = "tree-sitter-pascal" }, { name = "tree-sitter-sql" }, @@ -1182,6 +1183,9 @@ commonlisp = [ dm = [ { name = "tree-sitter-dm" }, ] +erlang = [ + { name = "tree-sitter-language-pack" }, +] falkordb = [ { name = "falkordb" }, ] @@ -1351,6 +1355,8 @@ requires-dist = [ { name = "tree-sitter-json", specifier = ">=0.23,<0.26" }, { name = "tree-sitter-julia", specifier = ">=0.23,<0.25" }, { name = "tree-sitter-kotlin", specifier = ">=1.0,<2.0" }, + { name = "tree-sitter-language-pack", marker = "extra == 'all'", specifier = "==0.11.0" }, + { name = "tree-sitter-language-pack", marker = "extra == 'erlang'", specifier = "==0.11.0" }, { name = "tree-sitter-lua", specifier = ">=0.2,<0.6" }, { name = "tree-sitter-objc", specifier = ">=3.0,<4.0" }, { name = "tree-sitter-ocaml", marker = "extra == 'all'" }, @@ -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", "erlang", "all"] [package.metadata.requires-dev] dev = [ @@ -4698,6 +4704,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/f2/711baae88f98e3a30efee9383fbcb603a3188c20941643c71d3d3b936d66/tree_sitter_elixir-0.3.5-cp39-abi3-win_arm64.whl", hash = "sha256:fee42b90962e1e131cc31720f3038410291b2196ed231e00c1721597fc0567df", size = 164003, upload-time = "2026-03-02T13:31:08.013Z" }, ] +[[package]] +name = "tree-sitter-embedded-template" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/a7/77729fefab8b1b5690cfc54328f2f629d1c076d16daf32c96ba39d3a3a3a/tree_sitter_embedded_template-0.25.0.tar.gz", hash = "sha256:7d72d5e8a1d1d501a7c90e841b51f1449a90cc240be050e4fb85c22dab991d50", size = 14114, upload-time = "2025-08-29T00:42:51.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/9d/3e3c8ee0c019d3bace728300a1ca807c03df39e66cc51e9a5e7c9d1e1909/tree_sitter_embedded_template-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fa0d06467199aeb33fb3d6fa0665bf9b7d5a32621ffdaf37fd8249f8a8050649", size = 10266, upload-time = "2025-08-29T00:42:44.148Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ab/6d4e43b736b2a895d13baea3791dc8ce7245bedf4677df9e7deb22e23a2a/tree_sitter_embedded_template-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc7aacbc2985a5d7e7fe7334f44dffe24c38fb0a8295c4188a04cf21a3d64a73", size = 10650, upload-time = "2025-08-29T00:42:45.147Z" }, + { url = "https://files.pythonhosted.org/packages/9f/97/ea3d1ea4b320fe66e0468b9f6602966e544c9fe641882484f9105e50ee0c/tree_sitter_embedded_template-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7c88c3dd8b94b3c9efe8ae071ff6b1b936a27ac5f6e651845c3b9631fa4c1c2", size = 18268, upload-time = "2025-08-29T00:42:46.03Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/0f42ca894a8f7c298cf336080046ccc14c10e8f4ea46d455f640193181b2/tree_sitter_embedded_template-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:025f7ca84218dcd8455efc901bdbcc2689fb694f3a636c0448e322a23d4bc96b", size = 19068, upload-time = "2025-08-29T00:42:46.699Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2a/0b720bcae7c2dd0a44889c09e800a2f8eb08c496dede9f2b97683506c4c3/tree_sitter_embedded_template-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b5dc1aef6ffa3fae621fe037d85dd98948b597afba20df29d779c426be813ee5", size = 18518, upload-time = "2025-08-29T00:42:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/14/8a/d745071afa5e8bdf5b381cf84c4dc6be6c79dee6af8e0ff07476c3d8e4aa/tree_sitter_embedded_template-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d0a35cfe634c44981a516243bc039874580e02a2990669313730187ce83a5bc6", size = 18267, upload-time = "2025-08-29T00:42:48.635Z" }, + { url = "https://files.pythonhosted.org/packages/5d/74/728355e594fca140f793f234fdfec195366b6956b35754d00ea97ca18b21/tree_sitter_embedded_template-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:3e05a4ac013d54505e75ae48e1a0e9db9aab19949fe15d9f4c7345b11a84a069", size = 13049, upload-time = "2025-08-29T00:42:49.589Z" }, + { url = "https://files.pythonhosted.org/packages/d8/de/afac475e694d0e626b0808f3c86339c349cd15c5163a6a16a53cc11cf892/tree_sitter_embedded_template-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:2751d402179ac0e83f2065b249d8fe6df0718153f1636bcb6a02bde3e5730db9", size = 11978, upload-time = "2025-08-29T00:42:50.226Z" }, +] + [[package]] name = "tree-sitter-fortran" version = "0.6.0" @@ -4836,6 +4858,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/b9/12fa97f63d2b7517c6f5d16938f0c5bfe84d925c652c75ff1c5e29bf6a44/tree_sitter_kotlin-1.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:e030f127a7d07952907adb9070248bd42fb86dc76fd92744727551b50e131ee7", size = 310414, upload-time = "2025-01-09T19:02:16.23Z" }, ] +[[package]] +name = "tree-sitter-language-pack" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tree-sitter" }, + { name = "tree-sitter-c-sharp" }, + { name = "tree-sitter-embedded-template" }, + { name = "tree-sitter-yaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/20/b21748c3386effa68bc7ab6d2bd9b78eabe391adc5797b7d37b24930f3b3/tree_sitter_language_pack-0.11.0.tar.gz", hash = "sha256:228901ce64c2b08feabfeeb687d6eb9450825bb8bd8cdf9caffffb9e7939614a", size = 49131889, upload-time = "2025-11-12T07:56:27.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/2c/33ec4d1458db4173d49b7f273b62ed7ce2c173eee1d0331ead912d93ee6f/tree_sitter_language_pack-0.11.0-cp310-abi3-macosx_10_15_universal2.whl", hash = "sha256:58b0351dd43e6a91261ac1264e9794a83c20eb78baa839e42699499ece1667dc", size = 31473144, upload-time = "2025-11-12T07:56:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/67/d9/c8335131075f84cbac955f521b5194655dd7340574b87d3611ed8583ec20/tree_sitter_language_pack-0.11.0-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:b4205958666c597d8a1b74eb4c845764c3aebd5db3c2484c2a5d9701b3feef42", size = 19345909, upload-time = "2025-11-12T07:56:18.361Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9f/ae964c062274507225e5f35262919c8d2f186eaec4ea76d1b67ba9c2f696/tree_sitter_language_pack-0.11.0-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:d029f9dc282f66a25d35b13d23ff2cd7108cd0170b426e0c1f755892d022e68c", size = 19203423, upload-time = "2025-11-12T07:56:21.472Z" }, + { url = "https://files.pythonhosted.org/packages/82/bb/17ee0bafb1b9e24075fe528b7f8963c6855dac56ccead44bf7855af3fab8/tree_sitter_language_pack-0.11.0-cp310-abi3-win_amd64.whl", hash = "sha256:5cab767ba535c8bb09ac4c2db4b672f0ff67135083355f50713e7052fa37e6ef", size = 16188199, upload-time = "2025-11-12T07:56:24.185Z" }, +] + [[package]] name = "tree-sitter-lua" version = "0.5.0" @@ -5057,6 +5097,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/a3/229851168ec3997f1ced60b93edbeb294a0c2b3af2d71143469371c05851/tree_sitter_verilog-1.0.3-cp39-abi3-win_arm64.whl", hash = "sha256:11576eaa43f89266ab8869fb8d2fb1c22c8da74aa8dc82e67259d6560635c68f", size = 749282, upload-time = "2024-11-10T23:35:30.602Z" }, ] +[[package]] +name = "tree-sitter-yaml" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/b6/941d356ac70c90b9d2927375259e3a4204f38f7499ec6e7e8a95b9664689/tree_sitter_yaml-0.7.2.tar.gz", hash = "sha256:756db4c09c9d9e97c81699e8f941cb8ce4e51104927f6090eefe638ee567d32c", size = 84882, upload-time = "2025-10-07T14:40:36.071Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/29/c0b8dbff302c49ff4284666ffb6f2f21145006843bb4c3a9a85d0ec0b7ae/tree_sitter_yaml-0.7.2-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:7e269ddcfcab8edb14fbb1f1d34eed1e1e26888f78f94eedfe7cc98c60f8bc9f", size = 43898, upload-time = "2025-10-07T14:40:29.486Z" }, + { url = "https://files.pythonhosted.org/packages/18/0d/15a5add06b3932b5e4ce5f5e8e179197097decfe82a0ef000952c8b98216/tree_sitter_yaml-0.7.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:0807b7966e23ddf7dddc4545216e28b5a58cdadedcecca86b8d8c74271a07870", size = 44691, upload-time = "2025-10-07T14:40:30.369Z" }, + { url = "https://files.pythonhosted.org/packages/72/92/c4b896c90d08deb8308fadbad2210fdcc4c66c44ab4292eac4e80acb4b61/tree_sitter_yaml-0.7.2-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1a5c60c98b6c4c037aae023569f020d0c489fad8dc26fdfd5510363c9c29a41", size = 91430, upload-time = "2025-10-07T14:40:31.16Z" }, + { url = "https://files.pythonhosted.org/packages/89/59/61f1fed31eb6d46ff080b8c0d53658cf29e10263f41ef5fe34768908037a/tree_sitter_yaml-0.7.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88636d19d0654fd24f4f242eaaafa90f6f5ebdba8a62e4b32d251ed156c51a2a", size = 92428, upload-time = "2025-10-07T14:40:31.954Z" }, + { url = "https://files.pythonhosted.org/packages/e3/62/a33a04d19b7f9a0ded780b9c9fcc6279e37c5d00b89b00425bb807a22cc2/tree_sitter_yaml-0.7.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1d2e8f0bb14aa4537320952d0f9607eef3021d5aada8383c34ebeece17db1e06", size = 90580, upload-time = "2025-10-07T14:40:33.037Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e7/9525defa7b30792623f56b1fba9bbba361752348875b165b8975b87398fd/tree_sitter_yaml-0.7.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:74ca712c50fc9d7dbc68cb36b4a7811d6e67a5466b5a789f19bf8dd6084ef752", size = 90455, upload-time = "2025-10-07T14:40:33.778Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d6/8d1e1ace03db3b02e64e91daf21d1347941d1bbecc606a5473a1a605250d/tree_sitter_yaml-0.7.2-cp310-abi3-win_amd64.whl", hash = "sha256:7587b5ca00fc4f9a548eff649697a3b395370b2304b399ceefa2087d8a6c9186", size = 45514, upload-time = "2025-10-07T14:40:34.562Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c7/dcf3ea1c4f5da9b10353b9af4455d756c92d728a8f58f03c480d3ef0ead5/tree_sitter_yaml-0.7.2-cp310-abi3-win_arm64.whl", hash = "sha256:f63c227b18e7ce7587bce124578f0bbf1f890ac63d3e3cd027417574273642c4", size = 44065, upload-time = "2025-10-07T14:40:35.337Z" }, +] + [[package]] name = "tree-sitter-zig" version = "1.1.2"