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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion graphify/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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', '.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', '.bats', '.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'}
Expand Down
5 changes: 3 additions & 2 deletions graphify/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -2555,7 +2555,7 @@ def _lang_is_case_insensitive(source_file: object) -> bool:
".ex": "elixir", ".exs": "elixir",
".jl": "julia",
".dart": "dart",
".sh": "shell", ".bash": "shell",
".sh": "shell", ".bash": "shell", ".bats": "shell",
".ps1": "powershell", ".psm1": "powershell", ".psd1": "powershell",
}

Expand Down Expand Up @@ -5768,6 +5768,7 @@ def add_existing_edge(edge: dict) -> None:
".lpk": extract_lazarus_package,
".sh": extract_bash,
".bash": extract_bash,
".bats": extract_bash,
".json": extract_json,
".tf": extract_terraform,
".tfvars": extract_terraform,
Expand Down Expand Up @@ -7057,7 +7058,7 @@ def _looks_like_bash(result: object) -> bool:

sh_pairs = [
(r, p) for r, p in zip(per_file, paths)
if p.suffix in (".sh", ".bash") or _looks_like_bash(r)
if p.suffix in (".sh", ".bash", ".bats") or _looks_like_bash(r)
]
if sh_pairs:
sh_results = [r for r, _ in sh_pairs]
Expand Down
29 changes: 24 additions & 5 deletions graphify/extractors/bash.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,17 +283,24 @@ def walk(node, parent_nid: str) -> None:
if t == "function_definition":
name = _bash_func_name(node)
if name:
fn_nid = _make_id(stem, name)
line = node.start_point[0] + 1
add_node(fn_nid, f"{name}()", line, kind="bash_function")
add_edge(parent_nid, fn_nid, "defines", line)
defined_functions.add(name)
# find the compound_statement body
body = None
for child in node.children:
if child.type == "compound_statement":
body = child
break

kind = "bash_function"
if body is not None:
if "#@test" in _read_text(body, source):
kind = "bash_test"

fn_nid = _make_id(stem, name)
line = node.start_point[0] + 1
add_node(fn_nid, f"{name}()", line, kind=kind)
add_edge(parent_nid, fn_nid, "defines", line)
defined_functions.add(name)

function_bodies.append((fn_nid, body))
# Recurse into the body so nested function definitions are discovered
# and added to function_bodies for the second-pass walk_calls.
Expand All @@ -312,6 +319,18 @@ def walk(node, parent_nid: str) -> None:
args = [c for c in node.children
if c.type in ("word", "string", "concatenation")
and c != cmd_name_node]

if cmd == "@test" and args:
desc = _read_text(args[0], source).strip().strip("'\"")
if desc:
test_nid = _make_id(stem, desc)
line = node.start_point[0] + 1
add_node(test_nid, desc, line, kind="bash_test")
add_edge(parent_nid, test_nid, "defines", line)
# The test body is parsed as subsequent arguments to the command
function_bodies.append((test_nid, node))
# We don't return here so `walk()` will still recurse into the children
# in case there are nested function definitions inside the test block.
if cmd in _BASH_SOURCE_COMMANDS and cmd not in defined_functions:
# find the path argument (first word after command name)
if args:
Expand Down
30 changes: 30 additions & 0 deletions tests/test_extract_bats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from pathlib import Path
from graphify.extractors.bash import extract_bash
import pytest

def test_bats_native_and_comment_forms(tmp_path):
src = b'''
@test "addition using bc" {
result="$(echo 2+2 | bc)"
[ "$result" -eq 4 ]
}

function invoking_foo_without_arguments_prints_usage { #@test
run foo
[ "$status" -eq 1 ]
}
'''
bats_file = tmp_path / 'dummy.bats'
bats_file.write_bytes(src)

res = extract_bash(bats_file)
if res.get('error'):
pytest.skip(res['error'])

nodes = {n['id']: n for n in res['nodes']}

assert 'dummy.bats:addition using bc' in nodes, "Missing native test node"
assert nodes['dummy.bats:addition using bc']['kind'] == 'bash_test'

assert 'dummy.bats:invoking_foo_without_arguments_prints_usage' in nodes, "Missing comment-form test node"
assert nodes['dummy.bats:invoking_foo_without_arguments_prints_usage']['kind'] == 'bash_test'