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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### New Features

- CodeGraph now indexes **Shell / Bash** (`.sh`, `.bash`, `.zsh`, `.ksh`, `.bats`) — functions in both the `foo()` and `function foo` forms, call edges between them, and `source` / `.` file dependency edges. A shell codebase is mostly small files wired together by `source`, so this is where the graph pays: `codegraph callers log_hook_audit` finds every script that calls a library function through its `source` line, and `codegraph impact` gives the blast radius of editing one. Command words that can never be a function (`echo`, `printf`, `local`, and the rest of the builtins) are left out so the real edges are not buried, while calls to external tools (`git`, `jq`) are kept — those answer "which scripts shell out to this". Sourced paths are matched through the usual `source "$SCRIPT_DIR/lib/_log.sh"` expansion. File-scope variables and `readonly` constants are indexed; function-local `local x=…` is not. (#239)

- Anonymous usage telemetry is now stored entirely on CodeGraph's own first-party infrastructure — no third-party analytics vendor receives any of it, and the endpoint that receives it makes no outbound requests at all. Individual events are deleted after 90 days, leaving only anonymous daily totals. Nothing about what is collected changed, your IP address is still never read or stored, and every off-switch works exactly as before (`codegraph telemetry off`, `CODEGRAPH_TELEMETRY=0`, `DO_NOT_TRACK=1`). `TELEMETRY.md` remains the complete field-by-field list.

- `codegraph_explore` no longer re-sends source it already returned earlier in the same conversation. A file it has already shown you comes back as a short pointer — the path, the symbols and the exact line range, with confirmation that the file hasn't changed since — and the space that frees is spent on code you haven't seen yet, so a follow-up call covers new ground instead of repeating the last one. If a file was edited in between, its source is always shown again in full. Set `CODEGRAPH_EXPLORE_DEDUP=0` to turn this off.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ CodeGraph's parsing engine is a **native Rust kernel**: 20 languages — TypeScr
| **Full-Text Search** | Find code by name instantly across your entire codebase, powered by FTS5 |
| **Impact Analysis** | Trace callers, callees, and the full impact radius of any symbol before making changes |
| **Always Fresh** | File watcher uses native OS events (FSEvents/inotify/ReadDirectoryChangesW) with debounced auto-sync — the graph stays current as you code, zero config |
| **20+ Languages** | TypeScript, JavaScript, ArkTS, Python, Go, Rust, Java, C#, VB.NET, PHP, Ruby, C, C++, CUDA, Objective-C, Metal, Swift, Kotlin, Scala, Dart, Lua, Luau, R, Nix, Erlang, CFML, COBOL, Solidity, Terraform/OpenTofu, Svelte, Vue, Astro, Liquid, Pascal/Delphi |
| **20+ Languages** | TypeScript, JavaScript, ArkTS, Python, Go, Rust, Java, C#, VB.NET, PHP, Ruby, C, C++, CUDA, Objective-C, Metal, Swift, Kotlin, Scala, Dart, Lua, Luau, R, Nix, Erlang, CFML, COBOL, Solidity, Terraform/OpenTofu, Shell/Bash, Svelte, Vue, Astro, Liquid, Pascal/Delphi |
| **Framework-aware Routes** | Recognizes web-framework routing files and links URL patterns to their handlers across 17 frameworks |
| **Mixed iOS / React Native / Expo** | Closes cross-language flows that static parsing misses: Swift ↔ ObjC bridging, React Native legacy bridge + TurboModules + Fabric view components, native → JS event emitters, Expo Modules |
| **100% Local** | No data leaves your machine. No API keys. No external services. SQLite database only |
Expand Down Expand Up @@ -822,6 +822,7 @@ is written):
| Erlang | `.erl`, `.hrl`, `.escript`, `.app.src`, `.app` | Full support (functions with multi-clause/multi-arity grouping, `-spec` signatures, records with fields, `-type`/`-opaque` aliases, `-define` macros, `-include`/`-include_lib`/`-import` edges, local and `mod:fn` remote call edges, `fun name/arity` references, `spawn`/`apply`/`proc_lib`/`timer`/`rpc` MFA-argument call edges, `gen_server:call/cast(?MODULE)` → own `handle_call`/`handle_cast` links, `-behaviour` links, `-export`-based visibility) |
| Solidity | `.sol` | Full support (contracts, libraries, interfaces, structs, enums, modifiers, events, errors, state variables, `import`/`using` directives, `emit`/`revert` calls) |
| Terraform / OpenTofu | `.tf`, `.tfvars`, `.tofu` | Full support (resources, data sources, modules, variables, outputs, providers incl. aliases, `locals`; `var.`/`local.`/`module.`/resource references with Terraform's per-directory scoping enforced; module calls bridged across the boundary — inputs to the child module's variables, `module.M.out` to the child's output, `source` to the module's files; cloudposse/atmos `remote-state` cross-component wiring when the component is statically named; `provider = aws.east` selections resolved up the module tree; `moved`/`import`/`removed`/`check` block references; `.tfvars` assignments linked to the variables they set) |
| Shell / Bash | `.sh`, `.bash`, `.zsh`, `.ksh`, `.bats` | Full support (both `foo()` and `function foo` definition forms, call edges on command words with shell builtins filtered out so real edges are not buried, `source`/`.` file dependency edges resolved through the usual `"$SCRIPT_DIR/lib/x.sh"` expansion, file-scope variables and `readonly` constants; function-local `local x=…` is deliberately not indexed) |
| Nix | `.nix` | Full support (functions with simple/destructured/curried params, `let`/attrset bindings, `inherit`, `import ./path` file edges — `./dir` resolving through `default.nix` — plus NixOS module `imports = [ ./x.nix ]` lists and `callPackage ./pkg.nix` file edges; call edges; module-system option wiring — a config write like `launchd.user.agents.x = { ... }` links to the module declaring `options.launchd.user.agents`, so option flows trace across modules) |

## Measured cross-file coverage
Expand Down
122 changes: 122 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ describe('Language Detection', () => {
expect(detectLanguage('config.mjs')).toBe('javascript');
});

it('should detect shell scripts', () => {
expect(detectLanguage('hooks/pre-commit.sh')).toBe('bash');
expect(detectLanguage('lib/_log.bash')).toBe('bash');
expect(detectLanguage('setup.zsh')).toBe('bash');
expect(detectLanguage('test/run.bats')).toBe('bash');
});

it('should detect Python files', () => {
expect(detectLanguage('main.py')).toBe('python');
});
Expand Down Expand Up @@ -11637,3 +11644,118 @@ describe('C/C++ kernel-port preParse blanks (R7a)', () => {
expect(result.nodes.some((n) => n.kind === 'method' && n.name === 'size')).toBe(true);
});
});

describe('Shell (bash) Extraction', () => {
it('should extract both function definition forms', () => {
const code = `#!/usr/bin/env bash
log_info() {
printf '%s\\n' "$1" >&2
}

function log_error {
log_info "error: $*"
return 1
}
`;
const result = extractFromSource('lib/_log.sh', code);

expect(result.errors).toEqual([]);
const fns = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
expect(fns).toContain('log_info');
expect(fns).toContain('log_error');
});

it('should extract calls to shell functions and external tools, but not builtins', () => {
const code = `#!/usr/bin/env bash
resolve_repo() {
git -C "$1" rev-parse --show-toplevel
}

main() {
local repo
repo="$(resolve_repo "$@")"
echo "$repo"
printf '%s\\n' "$repo"
}
`;
const result = extractFromSource('main.sh', code);
const calls = result.unresolvedReferences
.filter((r) => r.referenceKind === 'calls')
.map((r) => r.referenceName);

expect(calls).toContain('resolve_repo');
expect(calls).toContain('git');
// Builtins can never resolve to a definition and would bury the real edges.
expect(calls).not.toContain('echo');
expect(calls).not.toContain('printf');
expect(calls).not.toContain('local');
});

it('should extract `source` and `.` as imports, keeping the literal path tail', () => {
const code = `#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
source "$(dirname "\${BASH_SOURCE[0]}")/lib/_recover-cwd.sh"
. "$SCRIPT_DIR/lib/_log.sh"
source "\${SCRIPT_DIR}/lib/_git.sh"
`;
const result = extractFromSource('hook.sh', code);
const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);

expect(imports).toContain('lib/_recover-cwd.sh');
expect(imports).toContain('lib/_log.sh');
expect(imports).toContain('lib/_git.sh');
});

it('should find a `source` inside a function body', () => {
const code = `#!/usr/bin/env bash
load() {
source "$HERE/lib/_late.sh"
}
`;
const result = extractFromSource('late.sh', code);
const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
expect(imports).toContain('lib/_late.sh');
});

it('should extract file-scope variables and readonly constants, not locals', () => {
const code = `#!/usr/bin/env bash
TOP_LEVEL=1
readonly CONST_X=5
export EXPORTED_Y="a"

f() {
local inner_a=1
echo "$inner_a"
}
`;
const result = extractFromSource('vars.sh', code);
const vars = result.nodes.filter((n) => n.kind === 'variable').map((n) => n.name);
const consts = result.nodes.filter((n) => n.kind === 'constant').map((n) => n.name);

expect(vars).toContain('TOP_LEVEL');
expect(vars).toContain('EXPORTED_Y');
expect(consts).toContain('CONST_X');
expect(vars).not.toContain('inner_a');
});

it('should parse a case statement without erroring', () => {
// The tree-sitter-wasms bash build (ABI 14) traps on `case` under
// web-tree-sitter 0.25 and takes the whole file with it; this pins the
// vendored ABI-15 grammar.
const code = `#!/usr/bin/env bash
route() {
case "$1" in
fast) run_fast ;;
*) run_slow ;;
esac
}
`;
const result = extractFromSource('route.sh', code);
expect(result.errors).toEqual([]);
const calls = result.unresolvedReferences
.filter((r) => r.referenceKind === 'calls')
.map((r) => r.referenceName);
expect(calls).toContain('run_fast');
expect(calls).toContain('run_slow');
});
});
17 changes: 17 additions & 0 deletions src/extraction/grammars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const WASM_GRAMMAR_FILES: Record<GrammarLanguage, string> = {
terraform: 'tree-sitter-terraform.wasm',
arkts: 'tree-sitter-arkts.wasm',
nix: 'tree-sitter-nix.wasm',
bash: 'tree-sitter-bash.wasm',
};

/**
Expand Down Expand Up @@ -120,6 +121,15 @@ export const EXTENSION_MAP: Record<string, Language> = {
'.scala': 'scala',
'.sc': 'scala',
'.lua': 'lua',
// Shell: one grammar covers sh/bash/ksh dialects, and zsh scripts parse
// close enough that functions and calls extract (zsh-only syntax degrades to
// ERROR nodes locally rather than failing the file). `.bats` is bats-core
// test suites, which are plain bash plus `@test` blocks.
'.sh': 'bash',
'.bash': 'bash',
'.zsh': 'bash',
'.ksh': 'bash',
'.bats': 'bash',
'.luau': 'luau',
'.m': 'objc',
'.mm': 'objc',
Expand Down Expand Up @@ -338,6 +348,12 @@ const VENDORED_WASM_LANGS: ReadonlySet<GrammarLanguage> = new Set([
// kernel compiles the same-commit vendored C (codegraph-kernel/grammars/
// dart); crates.io tree-sitter-dart is a different-lineage fork (rejected).
'dart',
// Bash: the tree-sitter-wasms build is ABI 14 and traps on `case` statements
// under web-tree-sitter 0.25 (a missing dynamic-linking stub aborts the whole
// parse), so any real script dies on its first `case`. We vendor the prebuilt
// tree-sitter-bash.wasm from the tree-sitter-bash 0.25.1 npm package (MIT) —
// byte-identical to the npm tarball's artifact, ABI 15, parses `case` cleanly.
'bash',
]);

/** Absolute path of a language's grammar WASM (vendored or tree-sitter-wasms). */
Expand Down Expand Up @@ -654,6 +670,7 @@ export function getLanguageDisplayName(language: Language): string {
vbnet: 'Visual Basic .NET',
erlang: 'Erlang',
terraform: 'Terraform',
bash: 'Shell',
arkts: 'ArkTS',
unknown: 'Unknown',
};
Expand Down
Loading