Skip to content
Closed
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
30 changes: 29 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,34 @@ jobs:
- name: Run tests
run: npm test

typecheck:
runs-on: ubuntu-latest
name: TypeScript type check
steps:
- uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22

- name: Install dependencies
shell: bash
run: |
for attempt in 1 2 3; do
npm install && break
if [ "$attempt" -lt 3 ]; then
echo "::warning::npm install attempt $attempt failed, retrying in 15s..."
sleep 15
else
echo "::error::npm install failed after 3 attempts"
exit 1
fi
done

- name: Type check
run: npm run typecheck

rust-check:
runs-on: ubuntu-latest
name: Rust compile check
Expand All @@ -93,7 +121,7 @@ jobs:

ci-pipeline:
if: always()
needs: [lint, test, rust-check]
needs: [lint, test, typecheck, rust-check]
runs-on: ubuntu-latest
name: CI Testing Pipeline
steps:
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ jobs:
node scripts/sync-native-versions.js --strip
echo "Packaging version $VERSION"

- name: Build TypeScript
run: npm run build

- name: Disable prepublishOnly
run: npm pkg delete scripts.prepublishOnly

Expand Down Expand Up @@ -395,6 +398,9 @@ jobs:
node scripts/sync-native-versions.js
echo "Publishing version $VERSION"

- name: Build TypeScript
run: npm run build

- name: Disable prepublishOnly
run: npm pkg delete scripts.prepublishOnly

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
node_modules/
.codegraph/
dist/
.tsbuildinfo
*.db
coverage/
.env
Expand Down
20 changes: 13 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,23 @@
"version": "3.2.0",
"description": "Local code graph CLI — parse codebases with tree-sitter, build dependency graphs, query them",
"type": "module",
"main": "src/index.js",
"main": "dist/index.js",
"exports": {
".": {
"import": "./src/index.js",
"require": "./src/index.cjs",
"default": "./src/index.cjs"
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"default": "./dist/index.cjs"
},
"./cli": {
"import": "./src/cli.js"
"import": "./dist/cli.js"
},
"./package.json": "./package.json"
},
"bin": {
"codegraph": "./src/cli.js"
"codegraph": "./dist/cli.js"
},
"files": [
"dist/",
"src/",
"grammars/",
"LICENSE",
Expand All @@ -28,14 +29,18 @@
"node": ">=20"
},
"scripts": {
"build": "tsc",
"build:wasm": "node scripts/build-wasm.js",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"lint": "biome check src/ tests/",
"lint:fix": "biome check --write src/ tests/",
"format": "biome format --write src/ tests/",
"prepare": "npm run build:wasm && husky && npm run deps:tree",
"prepack": "npm run build",
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true});require('fs').rmSync('.tsbuildinfo',{force:true})\"",
"prepare": "npm run build:wasm && npm run build && husky && npm run deps:tree",
"deps:tree": "node scripts/gen-deps.cjs",
"release": "commit-and-tag-version",
"release:dry-run": "commit-and-tag-version --dry-run",
Expand Down Expand Up @@ -102,6 +107,7 @@
"tree-sitter-ruby": "^0.23.1",
"tree-sitter-rust": "^0.24.0",
"tree-sitter-typescript": "^0.23.2",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
},
"license": "Apache-2.0"
Expand Down
2 changes: 2 additions & 0 deletions src/cli/commands/diff-impact.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ export const command = {
['--staged', 'Analyze staged changes instead of unstaged'],
['--depth <n>', 'Max transitive caller depth', '3'],
['-f, --format <format>', 'Output format: text, mermaid, json', 'text'],
['--no-implementations', 'Exclude interface/trait implementors from blast radius'],
],
execute([ref], opts, ctx) {
diffImpact(opts.db, {
ref,
staged: opts.staged,
depth: parseInt(opts.depth, 10),
format: opts.format,
includeImplementors: opts.implementations !== false,
...ctx.resolveQueryOpts(opts),
});
},
Expand Down
2 changes: 2 additions & 0 deletions src/cli/commands/fn-impact.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const command = {
collectFile,
],
['-k, --kind <kind>', 'Filter to a specific symbol kind'],
['--no-implementations', 'Exclude interface/trait implementors from blast radius'],
],
validate([_name], opts) {
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
Expand All @@ -25,6 +26,7 @@ export const command = {
depth: parseInt(opts.depth, 10),
file: opts.file,
kind: opts.kind,
includeImplementors: opts.implementations !== false,
...ctx.resolveQueryOpts(opts),
});
},
Expand Down
29 changes: 29 additions & 0 deletions src/cli/commands/implementations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { collectFile } from '../../db/query-builder.js';
import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
import { implementations } from '../../presentation/queries-cli.js';

export const command = {
name: 'implementations <name>',
description: 'List all concrete types implementing a given interface or trait',
queryOpts: true,
options: [
[
'-f, --file <path>',
'Scope search to symbols in this file (partial match, repeatable)',
collectFile,
],
['-k, --kind <kind>', 'Filter to a specific symbol kind'],
],
validate([_name], opts) {
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
return `Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`;
}
},
execute([name], opts, ctx) {
implementations(name, opts.db, {
file: opts.file,
kind: opts.kind,
...ctx.resolveQueryOpts(opts),
});
},
};
29 changes: 29 additions & 0 deletions src/cli/commands/interfaces.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { collectFile } from '../../db/query-builder.js';
import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
import { interfaces } from '../../presentation/queries-cli.js';

export const command = {
name: 'interfaces <name>',
description: 'List all interfaces and traits that a class or struct implements',
queryOpts: true,
options: [
[
'-f, --file <path>',
'Scope search to symbols in this file (partial match, repeatable)',
collectFile,
],
['-k, --kind <kind>', 'Filter to a specific symbol kind'],
],
validate([_name], opts) {
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
return `Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`;
}
},
execute([name], opts, ctx) {
interfaces(name, opts.db, {
file: opts.file,
kind: opts.kind,
...ctx.resolveQueryOpts(opts),
});
},
};
2 changes: 2 additions & 0 deletions src/db/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ export {
findCrossFileCallTargets,
findDistinctCallers,
findFileNodes,
findImplementors,
findImportDependents,
findImportSources,
findImportTargets,
findInterfaces,
findIntraFileCallEdges,
findNodeById,
findNodeByQualifiedName,
Expand Down
38 changes: 38 additions & 0 deletions src/db/repository/edges.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const _findCrossFileCallTargetsStmt = new WeakMap();
const _countCrossFileCallersStmt = new WeakMap();
const _getClassAncestorsStmt = new WeakMap();
const _findIntraFileCallEdgesStmt = new WeakMap();
const _findImplementorsStmt = new WeakMap();
const _findInterfacesStmt = new WeakMap();

// ─── Call-edge queries ──────────────────────────────────────────────────

Expand Down Expand Up @@ -260,6 +262,42 @@ export function getClassHierarchy(db, classNodeId) {
return ancestors;
}

// ─── Implements-edge queries ──────────────────────────────────────────

/**
* Find all concrete types that implement a given interface/trait node.
* Follows incoming 'implements' edges (source = implementor, target = interface).
* @param {object} db
* @param {number} nodeId - The interface/trait node ID
* @returns {{ id: number, name: string, kind: string, file: string, line: number }[]}
*/
export function findImplementors(db, nodeId) {
return cachedStmt(
_findImplementorsStmt,
db,
`SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
FROM edges e JOIN nodes n ON e.source_id = n.id
WHERE e.target_id = ? AND e.kind = 'implements'`,
).all(nodeId);
}

/**
* Find all interfaces/traits that a given class/struct implements.
* Follows outgoing 'implements' edges (source = class, target = interface).
* @param {object} db
* @param {number} nodeId - The class/struct node ID
* @returns {{ id: number, name: string, kind: string, file: string, line: number }[]}
*/
export function findInterfaces(db, nodeId) {
return cachedStmt(
_findInterfacesStmt,
db,
`SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
FROM edges e JOIN nodes n ON e.target_id = n.id
WHERE e.source_id = ? AND e.kind = 'implements'`,
).all(nodeId);
}

/**
* Find intra-file call edges (caller → callee within the same file).
* Used by explainFileImpl for data flow visualization.
Expand Down
2 changes: 2 additions & 0 deletions src/db/repository/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ export {
findCallers,
findCrossFileCallTargets,
findDistinctCallers,
findImplementors,
findImportDependents,
findImportSources,
findImportTargets,
findInterfaces,
findIntraFileCallEdges,
getClassHierarchy,
} from './edges.js';
Expand Down
29 changes: 29 additions & 0 deletions src/domain/analysis/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import {
findCrossFileCallTargets,
findDbPath,
findFileNodes,
findImplementors,
findImportSources,
findImportTargets,
findInterfaces,
findIntraFileCallEdges,
findNodeChildren,
findNodesByFile,
Expand Down Expand Up @@ -107,6 +109,31 @@ function buildCallers(db, node, noTests) {
}));
}

const INTERFACE_LIKE_KINDS = new Set(['interface', 'trait']);
const IMPLEMENTOR_KINDS = new Set(['class', 'struct', 'record', 'enum']);

function buildImplementationInfo(db, node, noTests) {
// For interfaces/traits: show who implements them
if (INTERFACE_LIKE_KINDS.has(node.kind)) {
let impls = findImplementors(db, node.id);
if (noTests) impls = impls.filter((n) => !isTestFile(n.file));
return {
implementors: impls.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line })),
};
}
// For classes/structs: show what they implement
if (IMPLEMENTOR_KINDS.has(node.kind)) {
let ifaces = findInterfaces(db, node.id);
if (noTests) ifaces = ifaces.filter((n) => !isTestFile(n.file));
if (ifaces.length > 0) {
return {
implements: ifaces.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line })),
};
}
}
return {};
}

function buildRelatedTests(db, node, getFileLines, includeTests) {
const testCallerRows = findCallers(db, node.id);
const testCallers = testCallerRows.filter((c) => isTestFile(c.file));
Expand Down Expand Up @@ -337,6 +364,7 @@ export function contextData(name, customDbPath, opts = {}) {
const relatedTests = buildRelatedTests(db, node, getFileLines, includeTests);
const complexityMetrics = getComplexityMetrics(db, node.id);
const nodeChildren = getNodeChildrenSafe(db, node.id);
const implInfo = buildImplementationInfo(db, node, noTests);

return {
name: node.name,
Expand All @@ -352,6 +380,7 @@ export function contextData(name, customDbPath, opts = {}) {
callees,
callers,
relatedTests,
...implInfo,
};
});

Expand Down
Loading
Loading