clast-ai 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +11 -9
- package/README.md +447 -401
- package/bench/ab/onair-concept.json +22 -0
- package/bench/ab/onair-deadcode.json +7 -0
- package/bench/ab/onair-hard.json +32 -0
- package/bench/ab/onair-impact.json +17 -0
- package/bench/ab/onair-refactors.json +18 -0
- package/bench/ab/onair-survey.json +17 -0
- package/bench/ab/onair-tasks.json +20 -0
- package/bench/ab/onair.json +52 -0
- package/bench/ab/vscode-concept.json +22 -0
- package/bench/ab/vscode-hard.json +32 -0
- package/bench/ab/vscode-impact.json +17 -0
- package/bench/ab/vscode-refactors.json +18 -0
- package/bench/ab/vscode-tasks.json +20 -0
- package/bench/ab/vscode.json +52 -0
- package/bench/ab.mjs +213 -0
- package/bench/bench.mjs +98 -0
- package/bench/resolution.mjs +75 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +257 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/defaults.d.ts +12 -2
- package/dist/config/defaults.js +38 -31
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +26 -14
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +73 -13
- package/dist/db/index.js +481 -39
- package/dist/db/index.js.map +1 -1
- package/dist/engine.d.ts +15 -0
- package/dist/engine.js +28 -0
- package/dist/engine.js.map +1 -0
- package/dist/indexer/imports.d.ts +6 -0
- package/dist/indexer/imports.js +254 -0
- package/dist/indexer/imports.js.map +1 -0
- package/dist/indexer/index.d.ts +63 -0
- package/dist/indexer/index.js +360 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/process.d.ts +22 -0
- package/dist/indexer/process.js +42 -0
- package/dist/indexer/process.js.map +1 -0
- package/dist/indexer/scan.d.ts +43 -0
- package/dist/indexer/scan.js +223 -0
- package/dist/indexer/scan.js.map +1 -0
- package/dist/indexer/worker.d.ts +1 -0
- package/dist/indexer/worker.js +23 -0
- package/dist/indexer/worker.js.map +1 -0
- package/dist/llm/comment-generator.d.ts +17 -19
- package/dist/llm/comment-generator.js +46 -80
- package/dist/llm/comment-generator.js.map +1 -1
- package/dist/llm/index.d.ts +0 -1
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/prompts.d.ts +0 -1
- package/dist/llm/prompts.js +16 -16
- package/dist/parser/extractor.d.ts +21 -16
- package/dist/parser/extractor.js +700 -228
- package/dist/parser/extractor.js.map +1 -1
- package/dist/parser/index.d.ts +17 -21
- package/dist/parser/index.js +115 -171
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/langs/c.d.ts +3 -0
- package/dist/parser/langs/c.js +879 -0
- package/dist/parser/langs/c.js.map +1 -0
- package/dist/parser/langs/csharp.d.ts +2 -0
- package/dist/parser/langs/csharp.js +548 -0
- package/dist/parser/langs/csharp.js.map +1 -0
- package/dist/parser/langs/dart.d.ts +2 -0
- package/dist/parser/langs/dart.js +893 -0
- package/dist/parser/langs/dart.js.map +1 -0
- package/dist/parser/langs/go.d.ts +2 -0
- package/dist/parser/langs/go.js +455 -0
- package/dist/parser/langs/go.js.map +1 -0
- package/dist/parser/langs/java.d.ts +2 -0
- package/dist/parser/langs/java.js +390 -0
- package/dist/parser/langs/java.js.map +1 -0
- package/dist/parser/langs/php.d.ts +2 -0
- package/dist/parser/langs/php.js +486 -0
- package/dist/parser/langs/php.js.map +1 -0
- package/dist/parser/langs/python.d.ts +2 -0
- package/dist/parser/langs/python.js +216 -0
- package/dist/parser/langs/python.js.map +1 -0
- package/dist/parser/langs/ruby.d.ts +2 -0
- package/dist/parser/langs/ruby.js +602 -0
- package/dist/parser/langs/ruby.js.map +1 -0
- package/dist/parser/langs/rust.d.ts +2 -0
- package/dist/parser/langs/rust.js +931 -0
- package/dist/parser/langs/rust.js.map +1 -0
- package/dist/parser/langs/swift.d.ts +2 -0
- package/dist/parser/langs/swift.js +616 -0
- package/dist/parser/langs/swift.js.map +1 -0
- package/dist/parser/langs/typescript.d.ts +4 -0
- package/dist/parser/langs/typescript.js +535 -0
- package/dist/parser/langs/typescript.js.map +1 -0
- package/dist/parser/registry.d.ts +16 -0
- package/dist/parser/registry.js +87 -0
- package/dist/parser/registry.js.map +1 -0
- package/dist/parser/returns.d.ts +9 -0
- package/dist/parser/returns.js +152 -0
- package/dist/parser/returns.js.map +1 -0
- package/dist/parser/spec.d.ts +165 -0
- package/dist/parser/spec.js +188 -0
- package/dist/parser/spec.js.map +1 -0
- package/dist/query/arity.d.ts +27 -0
- package/dist/query/arity.js +187 -0
- package/dist/query/arity.js.map +1 -0
- package/dist/query/callgraph.d.ts +113 -0
- package/dist/query/callgraph.js +332 -0
- package/dist/query/callgraph.js.map +1 -0
- package/dist/query/format.d.ts +36 -0
- package/dist/query/format.js +136 -0
- package/dist/query/format.js.map +1 -0
- package/dist/query/hierarchy.d.ts +44 -0
- package/dist/query/hierarchy.js +441 -0
- package/dist/query/hierarchy.js.map +1 -0
- package/dist/query/resolve.d.ts +265 -0
- package/dist/query/resolve.js +1957 -0
- package/dist/query/resolve.js.map +1 -0
- package/dist/query/search.d.ts +35 -0
- package/dist/query/search.js +379 -0
- package/dist/query/search.js.map +1 -0
- package/dist/query/words.d.ts +30 -0
- package/dist/query/words.js +201 -0
- package/dist/query/words.js.map +1 -0
- package/dist/server/index.d.ts +1 -3
- package/dist/server/index.js +313 -73
- package/dist/server/index.js.map +1 -1
- package/dist/tools/context.d.ts +35 -0
- package/dist/tools/context.js +79 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/explore.d.ts +51 -0
- package/dist/tools/explore.js +779 -0
- package/dist/tools/explore.js.map +1 -0
- package/dist/tools/impact.d.ts +21 -0
- package/dist/tools/impact.js +743 -0
- package/dist/tools/impact.js.map +1 -0
- package/dist/tools/search.d.ts +59 -0
- package/dist/tools/search.js +843 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/stats.d.ts +26 -0
- package/dist/tools/stats.js +604 -0
- package/dist/tools/stats.js.map +1 -0
- package/dist/types.d.ts +95 -40
- package/dist/types.js +9 -1
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/watcher/index.d.ts +18 -16
- package/dist/watcher/index.js +94 -77
- package/dist/watcher/index.js.map +1 -1
- package/package.json +21 -24
- package/plugin/server.mjs +50 -0
- package/skills/audit/SKILL.md +37 -0
- package/skills/deadcode/SKILL.md +33 -0
- package/skills/explore/SKILL.md +54 -0
- package/skills/impact/SKILL.md +24 -0
- package/skills/status/SKILL.md +11 -0
- package/src/cli.ts +256 -0
- package/src/config/defaults.ts +50 -32
- package/src/config/index.ts +23 -14
- package/src/db/index.ts +540 -43
- package/src/engine.ts +30 -0
- package/src/indexer/imports.ts +241 -0
- package/src/indexer/index.ts +369 -0
- package/src/indexer/process.ts +63 -0
- package/src/indexer/scan.ts +226 -0
- package/src/indexer/worker.ts +24 -0
- package/src/llm/comment-generator.ts +52 -103
- package/src/llm/index.ts +80 -80
- package/src/llm/prompts.ts +95 -95
- package/src/parser/extractor.ts +643 -256
- package/src/parser/index.ts +105 -205
- package/src/parser/langs/c.ts +860 -0
- package/src/parser/langs/csharp.ts +519 -0
- package/src/parser/langs/dart.ts +820 -0
- package/src/parser/langs/go.ts +437 -0
- package/src/parser/langs/java.ts +361 -0
- package/src/parser/langs/php.ts +450 -0
- package/src/parser/langs/python.ts +206 -0
- package/src/parser/langs/ruby.ts +554 -0
- package/src/parser/langs/rust.ts +904 -0
- package/src/parser/langs/swift.ts +590 -0
- package/src/parser/langs/typescript.ts +499 -0
- package/src/parser/registry.ts +88 -0
- package/src/parser/returns.ts +137 -0
- package/src/parser/spec.ts +309 -0
- package/src/query/arity.ts +173 -0
- package/src/query/callgraph.ts +473 -0
- package/src/query/format.ts +136 -0
- package/src/query/hierarchy.ts +447 -0
- package/src/query/resolve.ts +1972 -0
- package/src/query/search.ts +430 -0
- package/src/query/words.ts +183 -0
- package/src/server/index.ts +390 -79
- package/src/tools/context.ts +82 -0
- package/src/tools/explore.ts +860 -0
- package/src/tools/impact.ts +748 -0
- package/src/tools/search.ts +886 -0
- package/src/tools/stats.ts +633 -0
- package/src/types.ts +113 -45
- package/src/version.ts +2 -0
- package/src/watcher/index.ts +82 -88
- package/tsconfig.json +7 -4
- package/.mcp.json +0 -8
- package/commands/clast-status.md +0 -9
- package/dist/config/defaults.d.ts.map +0 -1
- package/dist/config/index.d.ts.map +0 -1
- package/dist/db/index.d.ts.map +0 -1
- package/dist/db/queries.d.ts +0 -44
- package/dist/db/queries.d.ts.map +0 -1
- package/dist/db/queries.js +0 -251
- package/dist/db/queries.js.map +0 -1
- package/dist/db/schema.d.ts +0 -3
- package/dist/db/schema.d.ts.map +0 -1
- package/dist/db/schema.js +0 -94
- package/dist/db/schema.js.map +0 -1
- package/dist/db/types.d.ts +0 -44
- package/dist/db/types.d.ts.map +0 -1
- package/dist/db/types.js +0 -2
- package/dist/db/types.js.map +0 -1
- package/dist/llm/comment-generator.d.ts.map +0 -1
- package/dist/llm/index.d.ts.map +0 -1
- package/dist/llm/prompts.d.ts.map +0 -1
- package/dist/parser/call-resolver.d.ts +0 -15
- package/dist/parser/call-resolver.d.ts.map +0 -1
- package/dist/parser/call-resolver.js +0 -16
- package/dist/parser/call-resolver.js.map +0 -1
- package/dist/parser/extractor.d.ts.map +0 -1
- package/dist/parser/index.d.ts.map +0 -1
- package/dist/parser/languages.d.ts +0 -11
- package/dist/parser/languages.d.ts.map +0 -1
- package/dist/parser/languages.js +0 -74
- package/dist/parser/languages.js.map +0 -1
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/register-tools.d.ts +0 -7
- package/dist/server/register-tools.d.ts.map +0 -1
- package/dist/server/register-tools.js +0 -17
- package/dist/server/register-tools.js.map +0 -1
- package/dist/server/tools/clast-call-graph.d.ts +0 -4
- package/dist/server/tools/clast-call-graph.d.ts.map +0 -1
- package/dist/server/tools/clast-call-graph.js +0 -70
- package/dist/server/tools/clast-call-graph.js.map +0 -1
- package/dist/server/tools/clast-comment.d.ts +0 -5
- package/dist/server/tools/clast-comment.d.ts.map +0 -1
- package/dist/server/tools/clast-comment.js +0 -64
- package/dist/server/tools/clast-comment.js.map +0 -1
- package/dist/server/tools/clast-file-summary.d.ts +0 -4
- package/dist/server/tools/clast-file-summary.d.ts.map +0 -1
- package/dist/server/tools/clast-file-summary.js +0 -42
- package/dist/server/tools/clast-file-summary.js.map +0 -1
- package/dist/server/tools/clast-get-context.d.ts +0 -4
- package/dist/server/tools/clast-get-context.d.ts.map +0 -1
- package/dist/server/tools/clast-get-context.js +0 -80
- package/dist/server/tools/clast-get-context.js.map +0 -1
- package/dist/server/tools/clast-reindex.d.ts +0 -5
- package/dist/server/tools/clast-reindex.d.ts.map +0 -1
- package/dist/server/tools/clast-reindex.js +0 -36
- package/dist/server/tools/clast-reindex.js.map +0 -1
- package/dist/server/tools/clast-search.d.ts +0 -4
- package/dist/server/tools/clast-search.d.ts.map +0 -1
- package/dist/server/tools/clast-search.js +0 -29
- package/dist/server/tools/clast-search.js.map +0 -1
- package/dist/server/tools/clast-status.d.ts +0 -5
- package/dist/server/tools/clast-status.d.ts.map +0 -1
- package/dist/server/tools/clast-status.js +0 -23
- package/dist/server/tools/clast-status.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/watcher/gitignore.d.ts +0 -9
- package/dist/watcher/gitignore.d.ts.map +0 -1
- package/dist/watcher/gitignore.js +0 -38
- package/dist/watcher/gitignore.js.map +0 -1
- package/dist/watcher/index.d.ts.map +0 -1
- package/skills/clast-explore/SKILL.md +0 -41
- package/src/db/queries.ts +0 -332
- package/src/db/schema.ts +0 -106
- package/src/db/types.ts +0 -47
- package/src/parser/call-resolver.ts +0 -17
- package/src/parser/languages.ts +0 -86
- package/src/server/register-tools.ts +0 -30
- package/src/server/tools/clast-call-graph.ts +0 -82
- package/src/server/tools/clast-comment.ts +0 -76
- package/src/server/tools/clast-file-summary.ts +0 -59
- package/src/server/tools/clast-get-context.ts +0 -97
- package/src/server/tools/clast-reindex.ts +0 -52
- package/src/server/tools/clast-search.ts +0 -38
- package/src/server/tools/clast-status.ts +0 -37
- package/src/watcher/gitignore.ts +0 -42
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { CONTAINER_KINDS, type SymbolRow } from '../types.js';
|
|
5
|
+
import { callersOf } from '../query/callgraph.js';
|
|
6
|
+
import { firstLine, isTestPath, isVendoredPath, loc, plural } from '../query/format.js';
|
|
7
|
+
import { findFiles, findSymbols, parseSymbolRef, pathMatches, type FileHit } from '../query/resolve.js';
|
|
8
|
+
import { splitIdentifier } from '../query/words.js';
|
|
9
|
+
import { ToolContext } from './context.js';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// clast_references
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export interface ReferencesArgs {
|
|
16
|
+
name: string;
|
|
17
|
+
/** Restrict to files under / matching this path. */
|
|
18
|
+
path?: string;
|
|
19
|
+
/** Max reference lines to print. Default 40. */
|
|
20
|
+
limit?: number;
|
|
21
|
+
includeTests?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function references(ctx: ToolContext, args: ReferencesArgs): Promise<string> {
|
|
25
|
+
const note = await ctx.ready();
|
|
26
|
+
const ref = parseSymbolRef(args.name);
|
|
27
|
+
const name = ref.name;
|
|
28
|
+
const limit = Math.max(1, Math.min(args.limit ?? 40, 500));
|
|
29
|
+
// References are matched case-sensitively: a definition spelled differently (LRU_CLOCK for lru_clock) is not theirs.
|
|
30
|
+
const defs = findSymbols(ctx.db, { ...ref, path: undefined }).filter(d => d.name === name);
|
|
31
|
+
const nameId = ctx.db.nameId(name, false);
|
|
32
|
+
if (nameId === undefined) {
|
|
33
|
+
return note + `"${name}": no occurrences in indexed code (identifiers in strings and comments are not indexed; use clast_grep for those).`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let rows = ctx.db.all<{ file_id: number; path: string; lines: string }>(
|
|
37
|
+
`SELECT o.file_id, f.path, o.lines FROM occ o JOIN files f ON f.id = o.file_id WHERE o.name_id = ?`,
|
|
38
|
+
nameId
|
|
39
|
+
);
|
|
40
|
+
const pathFilter = args.path ?? ref.path;
|
|
41
|
+
if (pathFilter) rows = rows.filter(r => pathMatches(r.path, pathFilter));
|
|
42
|
+
if (args.includeTests === false) rows = rows.filter(r => !isTestPath(r.path));
|
|
43
|
+
|
|
44
|
+
const defFiles = new Set(defs.map(d => d.path));
|
|
45
|
+
rows.sort(
|
|
46
|
+
(a, b) =>
|
|
47
|
+
Number(defFiles.has(b.path)) - Number(defFiles.has(a.path)) ||
|
|
48
|
+
Number(isTestPath(a.path)) - Number(isTestPath(b.path)) ||
|
|
49
|
+
Number(isVendoredPath(a.path)) - Number(isVendoredPath(b.path)) ||
|
|
50
|
+
a.path.localeCompare(b.path)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const totalFiles = rows.length;
|
|
54
|
+
let totalLines = 0;
|
|
55
|
+
for (const r of rows) totalLines += r.lines.split(',').length;
|
|
56
|
+
|
|
57
|
+
const out: string[] = [];
|
|
58
|
+
out.push(`"${name}": ${plural(totalLines, 'reference line')} in ${plural(totalFiles, 'file')}`);
|
|
59
|
+
if (defs.length) {
|
|
60
|
+
out.push(`defined: ${defs.slice(0, 2).map(d => `${d.kind} ${d.qname} ${loc(d)}`).join('; ')}${defs.length > 2 ? `; +${defs.length - 2} more definitions of this name` : ''}`);
|
|
61
|
+
if (defs.length > 1) out.push('(references are matched by name, so they cover every definition of it)');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (ref.container.length) {
|
|
65
|
+
out.push(`(matched by the identifier "${name}" alone; the qualifier "${ref.container.join('.')}" is not used to filter usages)`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Definition lines of the name are context, not usages: they never eat the
|
|
69
|
+
// limit, and when many definitions share the name they are left out.
|
|
70
|
+
const allDefLines = new Set(ctx.db.symbolsByName(name).map(d => d.path + ':' + d.start_line));
|
|
71
|
+
const showDefs = allDefLines.size <= 3;
|
|
72
|
+
const perFile = totalFiles <= 3 ? limit : Math.max(4, Math.ceil(limit / 5));
|
|
73
|
+
let printed = 0;
|
|
74
|
+
let filesShown = 0;
|
|
75
|
+
let filesWithMore = 0;
|
|
76
|
+
for (const r of rows) {
|
|
77
|
+
if (printed >= limit) break;
|
|
78
|
+
await ctx.fresh([r.path]);
|
|
79
|
+
// Re-read the occurrence lines in case the file was just re-indexed.
|
|
80
|
+
const cur = ctx.db.get<{ lines: string }>(`SELECT lines FROM occ WHERE name_id = ? AND file_id = ?`, nameId, r.file_id);
|
|
81
|
+
if (!cur) continue;
|
|
82
|
+
const lineNums = cur.lines.split(',').map(Number);
|
|
83
|
+
const body: string[] = [];
|
|
84
|
+
let inFile = 0;
|
|
85
|
+
let skipped = 0;
|
|
86
|
+
for (const n of lineNums) {
|
|
87
|
+
const isDef = allDefLines.has(r.path + ':' + n);
|
|
88
|
+
if (isDef) {
|
|
89
|
+
if (showDefs) body.push(` ${String(n).padStart(5)} [def] ${ctx.lines.line(r.path, n, 150)}`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (printed >= limit || inFile >= perFile) {
|
|
93
|
+
skipped++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const enclosing = ctx.db.enclosingSymbol(r.file_id, n);
|
|
97
|
+
body.push(` ${String(n).padStart(5)} ${enclosing ? '[' + enclosing.qname + '] ' : ''}${ctx.lines.line(r.path, n, 150)}`);
|
|
98
|
+
printed++;
|
|
99
|
+
inFile++;
|
|
100
|
+
}
|
|
101
|
+
if (body.length === 0) continue;
|
|
102
|
+
out.push(r.path);
|
|
103
|
+
out.push(...body);
|
|
104
|
+
filesShown++;
|
|
105
|
+
if (skipped) {
|
|
106
|
+
out.push(` ... ${skipped} more in this file`);
|
|
107
|
+
filesWithMore++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const rest = rows.length - filesShown;
|
|
111
|
+
if (rest > 0 && printed >= limit) {
|
|
112
|
+
const remaining = rows.slice(filesShown).filter(r => !out.includes(r.path));
|
|
113
|
+
out.push(`... ${plural(remaining.length, 'more file')}: ${remaining.slice(0, 12).map(r => r.path).join(', ')}${remaining.length > 12 ? ', ...' : ''}`);
|
|
114
|
+
}
|
|
115
|
+
if ((rest > 0 && printed >= limit) || filesWithMore) out.push('(narrow with path, or raise limit)');
|
|
116
|
+
return note + out.join('\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// clast_file_summary
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
export interface FileSummaryArgs {
|
|
124
|
+
/** File or directory path (partial paths are resolved). */
|
|
125
|
+
path: string;
|
|
126
|
+
/** Include fields / properties / constants inside containers. Default: auto. */
|
|
127
|
+
members?: 'all' | 'callable' | 'auto';
|
|
128
|
+
/** Max outline lines for a file. Default 300. */
|
|
129
|
+
limit?: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const MINOR_KINDS = new Set(['property', 'field', 'constant', 'variable']);
|
|
133
|
+
|
|
134
|
+
function range(s: { start_line: number; end_line: number }): string {
|
|
135
|
+
return s.start_line === s.end_line ? `${s.start_line}` : `${s.start_line}-${s.end_line}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function outlineFile(ctx: ToolContext, file: FileHit, members: 'all' | 'callable' | 'auto', maxLines = 300): string[] {
|
|
139
|
+
const out: string[] = [];
|
|
140
|
+
const symbols = ctx.db.symbolsInFile(file.id);
|
|
141
|
+
out.push(`${file.path} (${file.lang}, ${plural(file.line_count, 'line')}, ${plural(symbols.length, 'symbol')})`);
|
|
142
|
+
|
|
143
|
+
const imports = ctx.db.all<{ source: string; resolved: string | null }>(
|
|
144
|
+
`SELECT i.source, f.path AS resolved FROM imports i LEFT JOIN files f ON f.id = i.resolved_id
|
|
145
|
+
WHERE i.file_id = ? ORDER BY i.line`,
|
|
146
|
+
file.id
|
|
147
|
+
);
|
|
148
|
+
if (imports.length) {
|
|
149
|
+
const internal = [...new Set(imports.filter(i => i.resolved).map(i => i.resolved!))];
|
|
150
|
+
const external = [...new Set(imports.filter(i => !i.resolved).map(i => i.source))];
|
|
151
|
+
if (internal.length) out.push(`imports (project): ${internal.slice(0, 20).join(', ')}${internal.length > 20 ? `, +${internal.length - 20}` : ''}`);
|
|
152
|
+
if (external.length) out.push(`imports (external): ${external.slice(0, 20).join(', ')}${external.length > 20 ? `, +${external.length - 20}` : ''}`);
|
|
153
|
+
}
|
|
154
|
+
const importedBy = ctx.db.all<{ path: string }>(
|
|
155
|
+
`SELECT DISTINCT f.path FROM imports i JOIN files f ON f.id = i.file_id WHERE i.resolved_id = ? ORDER BY f.path LIMIT 200`,
|
|
156
|
+
file.id
|
|
157
|
+
);
|
|
158
|
+
if (importedBy.length) {
|
|
159
|
+
out.push(`imported by (${importedBy.length}${importedBy.length === 200 ? '+' : ''}): ${importedBy.slice(0, 5).map(r => r.path).join(', ')}${importedBy.length > 5 ? ', ...' : ''}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const hideMinor = members === 'callable' || (members === 'auto' && symbols.length > 150);
|
|
163
|
+
const depthOf = new Map<number, number>();
|
|
164
|
+
const hidden = new Map<number | null, number>();
|
|
165
|
+
const width = String(file.line_count).length * 2 + 1;
|
|
166
|
+
let lastLine = '';
|
|
167
|
+
let printed = 0;
|
|
168
|
+
let omitted = 0;
|
|
169
|
+
for (const s of symbols) {
|
|
170
|
+
const depth = s.parent_id !== null && depthOf.has(s.parent_id) ? depthOf.get(s.parent_id)! + 1 : 0;
|
|
171
|
+
depthOf.set(s.id, depth);
|
|
172
|
+
if (hideMinor && depth > 0 && MINOR_KINDS.has(s.kind)) {
|
|
173
|
+
hidden.set(s.parent_id, (hidden.get(s.parent_id) ?? 0) + 1);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// Ruby signatures do not carry visibility (it comes from a `private` section above).
|
|
177
|
+
const vis = s.lang === 'ruby' && s.kind === 'method' && !s.exported && !s.name.startsWith('_') ? 'private ' : '';
|
|
178
|
+
const label = vis + firstLine(s.signature || `${s.kind} ${s.name}`, 150);
|
|
179
|
+
// Members defined outside their type's body (Go receivers, Rust impl blocks, C++ Foo::bar):
|
|
180
|
+
// say which type they belong to when the signature does not.
|
|
181
|
+
const dot = s.qname.lastIndexOf('.');
|
|
182
|
+
const owner = depth === 0 && dot > 0 ? s.qname.slice(0, dot) : '';
|
|
183
|
+
const ownerLast = owner.slice(owner.lastIndexOf('.') + 1);
|
|
184
|
+
const ownerTag = owner && !label.includes(ownerLast) ? `[${owner}] ` : '';
|
|
185
|
+
const line =
|
|
186
|
+
`${range(s).padEnd(width)} ${' '.repeat(depth)}${ownerTag}${label}` +
|
|
187
|
+
`${s.doc ? ' // ' + firstLine(s.doc, 70) : ''}`;
|
|
188
|
+
if (line === lastLine) continue; // multi-declarator lines
|
|
189
|
+
lastLine = line;
|
|
190
|
+
if (printed >= maxLines) {
|
|
191
|
+
omitted++;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
printed++;
|
|
195
|
+
out.push(line);
|
|
196
|
+
}
|
|
197
|
+
if (omitted) {
|
|
198
|
+
out.push(
|
|
199
|
+
`... ${omitted} more symbols not shown (limit ${maxLines}). Narrow with clast_search path="${file.path}", ` +
|
|
200
|
+
`or fetch a region with clast_get_context "${file.path}:<line>".`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (hidden.size) {
|
|
204
|
+
let n = 0;
|
|
205
|
+
for (const v of hidden.values()) n += v;
|
|
206
|
+
out.push(`(${n} fields/properties/constants hidden; pass members: "all" to list them)`);
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function outlineDirectory(ctx: ToolContext, dir: string, files: FileHit[]): string[] {
|
|
212
|
+
const out: string[] = [];
|
|
213
|
+
const prefix = dir.endsWith('/') ? dir : dir + '/';
|
|
214
|
+
let totalLines = 0;
|
|
215
|
+
for (const f of files) totalLines += f.line_count;
|
|
216
|
+
out.push(`${prefix} (${plural(files.length, 'indexed file')}, ${totalLines} lines)`);
|
|
217
|
+
|
|
218
|
+
// Direct files get their top-level symbols; subdirectories are summarised.
|
|
219
|
+
const subdirs = new Map<string, { files: number; lines: number }>();
|
|
220
|
+
const direct: FileHit[] = [];
|
|
221
|
+
for (const f of files) {
|
|
222
|
+
const rest = f.path.slice(prefix.length);
|
|
223
|
+
const slash = rest.indexOf('/');
|
|
224
|
+
if (slash === -1) direct.push(f);
|
|
225
|
+
else {
|
|
226
|
+
const name = rest.slice(0, slash);
|
|
227
|
+
const agg = subdirs.get(name) ?? { files: 0, lines: 0 };
|
|
228
|
+
agg.files++;
|
|
229
|
+
agg.lines += f.line_count;
|
|
230
|
+
subdirs.set(name, agg);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (subdirs.size) {
|
|
234
|
+
out.push('subdirectories:');
|
|
235
|
+
for (const [name, agg] of [...subdirs.entries()].sort((a, b) => b[1].files - a[1].files).slice(0, 40)) {
|
|
236
|
+
out.push(` ${name}/ ${plural(agg.files, 'file')}, ${agg.lines} lines`);
|
|
237
|
+
}
|
|
238
|
+
if (subdirs.size > 40) out.push(` ... ${subdirs.size - 40} more`);
|
|
239
|
+
}
|
|
240
|
+
const shown = direct.slice(0, 60);
|
|
241
|
+
if (shown.length) out.push('files:');
|
|
242
|
+
for (const f of shown) {
|
|
243
|
+
const top = ctx.db.all<{ name: string; kind: string }>(
|
|
244
|
+
`SELECT name, kind FROM symbols WHERE file_id = ? AND parent_id IS NULL
|
|
245
|
+
AND kind NOT IN ('variable', 'constant', 'property', 'field')
|
|
246
|
+
ORDER BY (kind IN ('class','interface','struct','trait','enum')) DESC, exported DESC, start_line LIMIT 9`,
|
|
247
|
+
f.id
|
|
248
|
+
);
|
|
249
|
+
const names = top.slice(0, 8).map(t => t.name).join(', ');
|
|
250
|
+
out.push(` ${f.path.slice(prefix.length)} (${f.line_count} lines)${names ? ': ' + names + (top.length > 8 ? ', ...' : '') : ''}`);
|
|
251
|
+
}
|
|
252
|
+
if (direct.length > shown.length) out.push(` ... ${direct.length - shown.length} more files`);
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function fileSummary(ctx: ToolContext, args: FileSummaryArgs): Promise<string> {
|
|
257
|
+
const note = await ctx.ready();
|
|
258
|
+
const q = args.path.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
259
|
+
const cols = 'id, path, lang, line_count, symbol_count';
|
|
260
|
+
|
|
261
|
+
// Directory?
|
|
262
|
+
if (q === '' || q === '.') {
|
|
263
|
+
const all = ctx.db.all<FileHit>(`SELECT ${cols} FROM files ORDER BY path`);
|
|
264
|
+
return note + outlineDirectory(ctx, '', all).join('\n').replace(/^\/ /, './ ');
|
|
265
|
+
}
|
|
266
|
+
const dirFiles = ctx.db.all<FileHit>(
|
|
267
|
+
`SELECT ${cols} FROM files WHERE path >= ? AND path < ? ORDER BY path`,
|
|
268
|
+
q + '/', q + '/\uffff'
|
|
269
|
+
);
|
|
270
|
+
if (dirFiles.length) return note + outlineDirectory(ctx, q, dirFiles).join('\n');
|
|
271
|
+
|
|
272
|
+
const files = findFiles(ctx.db, q, 12);
|
|
273
|
+
if (files.length === 0) {
|
|
274
|
+
return note + `"${args.path}": no indexed file or directory matches. Use clast_overview to see the project layout.`;
|
|
275
|
+
}
|
|
276
|
+
if (files.length > 1) {
|
|
277
|
+
return (
|
|
278
|
+
note +
|
|
279
|
+
`"${args.path}" matches ${files.length}${files.length === 12 ? '+' : ''} files; be more specific:\n` +
|
|
280
|
+
files.map(f => ` ${f.path}`).join('\n')
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
await ctx.fresh([files[0]!.path]);
|
|
284
|
+
const file = findFiles(ctx.db, files[0]!.path, 1)[0];
|
|
285
|
+
if (!file) return note + `"${args.path}": file is no longer indexable.`;
|
|
286
|
+
return note + outlineFile(ctx, file, args.members ?? 'auto', Math.max(20, Math.min(args.limit ?? 300, 5000))).join('\n');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// clast_overview
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
export interface OverviewArgs {
|
|
294
|
+
/** Limit the overview to a subdirectory. */
|
|
295
|
+
path?: string;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const overviewCache = new WeakMap<object, Map<string, { gen: number; text: string }>>();
|
|
299
|
+
|
|
300
|
+
export async function overview(ctx: ToolContext, args: OverviewArgs = {}): Promise<string> {
|
|
301
|
+
const note = await ctx.ready();
|
|
302
|
+
const scope = (args.path ?? '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
303
|
+
let cache = overviewCache.get(ctx.db);
|
|
304
|
+
if (!cache) overviewCache.set(ctx.db, (cache = new Map()));
|
|
305
|
+
const cached = cache.get(scope);
|
|
306
|
+
if (cached && cached.gen === ctx.db.generation) return note + cached.text;
|
|
307
|
+
|
|
308
|
+
const prefix = scope ? scope + '/' : '';
|
|
309
|
+
const files = ctx.db.all<{ id: number; path: string; lang: string; line_count: number; symbol_count: number }>(
|
|
310
|
+
scope
|
|
311
|
+
? `SELECT id, path, lang, line_count, symbol_count FROM files WHERE path >= ? AND path < ? ORDER BY path`
|
|
312
|
+
: `SELECT id, path, lang, line_count, symbol_count FROM files ORDER BY path`,
|
|
313
|
+
...(scope ? [prefix, prefix + '\uffff'] : [])
|
|
314
|
+
);
|
|
315
|
+
if (files.length === 0) return note + (scope ? `no indexed files under "${scope}"` : 'the index is empty (no supported source files found)');
|
|
316
|
+
|
|
317
|
+
const out: string[] = [];
|
|
318
|
+
const langs = new Map<string, { files: number; lines: number }>();
|
|
319
|
+
let lines = 0;
|
|
320
|
+
let symbols = 0;
|
|
321
|
+
for (const f of files) {
|
|
322
|
+
const agg = langs.get(f.lang) ?? { files: 0, lines: 0 };
|
|
323
|
+
agg.files++;
|
|
324
|
+
agg.lines += f.line_count;
|
|
325
|
+
langs.set(f.lang, agg);
|
|
326
|
+
lines += f.line_count;
|
|
327
|
+
symbols += f.symbol_count;
|
|
328
|
+
}
|
|
329
|
+
out.push(`${scope || basename(ctx.clast.root)}: ${plural(files.length, 'file')}, ${lines} lines, ${symbols} symbols`);
|
|
330
|
+
out.push(
|
|
331
|
+
'languages: ' +
|
|
332
|
+
[...langs.entries()]
|
|
333
|
+
.sort((a, b) => b[1].lines - a[1].lines)
|
|
334
|
+
.slice(0, 6)
|
|
335
|
+
.map(([l, a]) => `${l} ${a.files} files/${a.lines} lines`)
|
|
336
|
+
.join(', ') +
|
|
337
|
+
(langs.size > 6 ? `, +${langs.size - 6} more` : '')
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
// Directory tree: two levels below the scope, single-child chains collapsed.
|
|
341
|
+
interface Dir { files: number; lines: number; children: Map<string, Dir> }
|
|
342
|
+
const root: Dir = { files: 0, lines: 0, children: new Map() };
|
|
343
|
+
for (const f of files) {
|
|
344
|
+
const parts = f.path.slice(prefix.length).split('/');
|
|
345
|
+
parts.pop();
|
|
346
|
+
let node = root;
|
|
347
|
+
node.files++;
|
|
348
|
+
node.lines += f.line_count;
|
|
349
|
+
for (const p of parts) {
|
|
350
|
+
let child = node.children.get(p);
|
|
351
|
+
if (!child) node.children.set(p, (child = { files: 0, lines: 0, children: new Map() }));
|
|
352
|
+
child.files++;
|
|
353
|
+
child.lines += f.line_count;
|
|
354
|
+
node = child;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
out.push('layout (files, lines):');
|
|
358
|
+
// Collapse a/b/c chains where each level has a single subdirectory and no files of its own.
|
|
359
|
+
interface View { label: string; dir: Dir; depth: number; expanded: boolean; test: boolean }
|
|
360
|
+
const collapse = (name: string, dir: Dir): { label: string; dir: Dir } => {
|
|
361
|
+
let label = name;
|
|
362
|
+
let cur = dir;
|
|
363
|
+
while (cur.children.size === 1) {
|
|
364
|
+
const [childName, child] = [...cur.children.entries()][0]!;
|
|
365
|
+
if (child.files !== cur.files) break;
|
|
366
|
+
label += '/' + childName;
|
|
367
|
+
cur = child;
|
|
368
|
+
}
|
|
369
|
+
return { label, dir: cur };
|
|
370
|
+
};
|
|
371
|
+
const viewOf = (name: string, dir: Dir, depth: number): View => {
|
|
372
|
+
const c = collapse(name, dir);
|
|
373
|
+
return { label: c.label, dir: c.dir, depth, expanded: false, test: isTestPath(c.label + '/') };
|
|
374
|
+
};
|
|
375
|
+
// Greedy expansion: keep opening the biggest non-test directory until the
|
|
376
|
+
// map reaches its line budget, so big repos get depth where the code is.
|
|
377
|
+
const LAYOUT_BUDGET = 44;
|
|
378
|
+
const CHILD_CAP = 8;
|
|
379
|
+
const tree = new Map<View, View[]>();
|
|
380
|
+
const rootViews = [...root.children.entries()]
|
|
381
|
+
.map(([n, d]) => viewOf(n, d, 1))
|
|
382
|
+
.sort((a, b) => Number(a.test) - Number(b.test) || b.dir.lines - a.dir.lines)
|
|
383
|
+
.slice(0, 14);
|
|
384
|
+
let lineCount = rootViews.length;
|
|
385
|
+
const open: View[] = [...rootViews];
|
|
386
|
+
while (lineCount < LAYOUT_BUDGET) {
|
|
387
|
+
let best: View | null = null;
|
|
388
|
+
for (const v of open) {
|
|
389
|
+
if (v.expanded || v.test || v.depth >= 4 || v.dir.children.size < 2) continue;
|
|
390
|
+
if (!best || v.dir.lines > best.dir.lines) best = v;
|
|
391
|
+
}
|
|
392
|
+
if (!best) break;
|
|
393
|
+
best.expanded = true;
|
|
394
|
+
const kids = [...best.dir.children.entries()]
|
|
395
|
+
.map(([n, d]) => viewOf(n, d, best!.depth + 1))
|
|
396
|
+
.sort((a, b) => Number(a.test) - Number(b.test) || b.dir.lines - a.dir.lines);
|
|
397
|
+
tree.set(best, kids);
|
|
398
|
+
const shown = Math.min(kids.length, CHILD_CAP);
|
|
399
|
+
lineCount += shown + (kids.length > shown ? 1 : 0);
|
|
400
|
+
open.push(...kids.slice(0, shown));
|
|
401
|
+
}
|
|
402
|
+
const renderView = (v: View): void => {
|
|
403
|
+
out.push(`${' '.repeat(v.depth)}${v.label}/ ${v.dir.files}, ${v.dir.lines}`);
|
|
404
|
+
const kids = tree.get(v);
|
|
405
|
+
if (!kids) return;
|
|
406
|
+
for (const k of kids.slice(0, CHILD_CAP)) renderView(k);
|
|
407
|
+
if (kids.length > CHILD_CAP) {
|
|
408
|
+
let restFiles = 0;
|
|
409
|
+
for (const k of kids.slice(CHILD_CAP)) restFiles += k.dir.files;
|
|
410
|
+
out.push(`${' '.repeat(v.depth + 1)}... ${kids.length - CHILD_CAP} more dirs (${restFiles} files)`);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
const rootFiles = files.filter(f => !f.path.slice(prefix.length).includes('/')).length;
|
|
414
|
+
if (rootFiles) out.push(` (${plural(rootFiles, 'file')} directly in ${scope || 'the root'})`);
|
|
415
|
+
for (const v of rootViews) renderView(v);
|
|
416
|
+
if (root.children.size > rootViews.length) out.push(` ... ${root.children.size - rootViews.length} more top-level dirs`);
|
|
417
|
+
|
|
418
|
+
// Key symbols: types and functions whose names are mentioned in the most files.
|
|
419
|
+
const popular = ctx.db.all<{ name: string; refs: number }>(
|
|
420
|
+
`SELECT n.name, t.refs
|
|
421
|
+
FROM (SELECT o.name_id, COUNT(*) AS refs FROM occ o GROUP BY o.name_id ORDER BY refs DESC LIMIT 2500) t
|
|
422
|
+
JOIN names n ON n.id = t.name_id
|
|
423
|
+
WHERE length(n.name) > 3
|
|
424
|
+
ORDER BY t.refs DESC`
|
|
425
|
+
);
|
|
426
|
+
const KEY_KINDS = new Set(['class', 'interface', 'struct', 'trait', 'enum', 'function', 'type']);
|
|
427
|
+
const resolvedImports =
|
|
428
|
+
ctx.db.get<{ n: number }>(`SELECT COUNT(*) AS n FROM imports WHERE resolved_id IS NOT NULL`)?.n ?? 0;
|
|
429
|
+
const hasImportGraph = resolvedImports > files.length / 2;
|
|
430
|
+
const keyLines: string[] = [];
|
|
431
|
+
for (const p of popular) {
|
|
432
|
+
// Inside a sub-tree only a handful of landmarks are worth the space.
|
|
433
|
+
if (keyLines.length >= (scope ? 10 : 25)) break;
|
|
434
|
+
if (scope && p.refs < 5) break;
|
|
435
|
+
const defs = findSymbols(ctx.db, { name: p.name, container: [] }, { exact: true }).filter(
|
|
436
|
+
s => KEY_KINDS.has(s.kind) && s.parent_id === null
|
|
437
|
+
);
|
|
438
|
+
// Names defined all over the place ("constructor", "main", "test") are not landmarks.
|
|
439
|
+
if (defs.length === 0 || defs.length > 8) continue;
|
|
440
|
+
const c = defs[0]!;
|
|
441
|
+
if (scope && !c.path.startsWith(prefix)) continue;
|
|
442
|
+
if (isTestPath(c.path) || isVendoredPath(c.path)) continue;
|
|
443
|
+
// Single common words ("filter", "join", "default") collect references that are not theirs.
|
|
444
|
+
if (c.kind === 'function' && splitIdentifier(c.name).length < 2) continue;
|
|
445
|
+
// A widely used symbol lives in a widely imported file. A popular *name*
|
|
446
|
+
// defined in a file nobody imports is a collision with a builtin or property.
|
|
447
|
+
if (hasImportGraph) {
|
|
448
|
+
const importers =
|
|
449
|
+
ctx.db.get<{ n: number }>(`SELECT COUNT(*) AS n FROM imports WHERE resolved_id = ?`, c.file_id)?.n ?? 0;
|
|
450
|
+
if (importers < Math.max(2, p.refs * 0.04)) continue;
|
|
451
|
+
if (defs.length > 1) {
|
|
452
|
+
// Several definitions share the name: this one must itself be imported by name
|
|
453
|
+
// often enough to own that popularity (an `Error` class next to a famous `Error`).
|
|
454
|
+
const byName = ctx.db.get<{ n: number }>(
|
|
455
|
+
`SELECT COUNT(DISTINCT file_id) AS n FROM imports WHERE resolved_id = ? AND (',' || names || ',') LIKE ? ESCAPE '\\'`,
|
|
456
|
+
c.file_id, '%,' + c.name.replace(/[\\%_]/g, ch => '\\' + ch) + ',%'
|
|
457
|
+
)?.n ?? 0;
|
|
458
|
+
if (byName < Math.max(2, p.refs * 0.03)) continue;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
keyLines.push(` ${c.kind} ${c.qname} ${c.path}:${c.start_line} refs:${p.refs}${c.doc ? ' // ' + firstLine(c.doc, 60) : ''}`);
|
|
462
|
+
}
|
|
463
|
+
if (keyLines.length) {
|
|
464
|
+
out.push('key symbols (most widely referenced):');
|
|
465
|
+
out.push(...keyLines);
|
|
466
|
+
}
|
|
467
|
+
// Hub files: imported by the most other files.
|
|
468
|
+
const hubs = ctx.db
|
|
469
|
+
.all<{ path: string; n: number }>(
|
|
470
|
+
`SELECT f.path, COUNT(DISTINCT i.file_id) AS n FROM imports i JOIN files f ON f.id = i.resolved_id
|
|
471
|
+
WHERE i.resolved_id IS NOT NULL GROUP BY i.resolved_id ORDER BY n DESC LIMIT 60`
|
|
472
|
+
)
|
|
473
|
+
.filter(h => !scope || h.path.startsWith(prefix))
|
|
474
|
+
.slice(0, 12);
|
|
475
|
+
if (hubs.length) {
|
|
476
|
+
out.push('hub files (imported by N files):');
|
|
477
|
+
for (const h of hubs) out.push(` ${h.path} ${h.n}`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Entry points.
|
|
481
|
+
const entries = ctx.db
|
|
482
|
+
.all<{ qname: string; path: string; start_line: number }>(
|
|
483
|
+
`SELECT s.qname, f.path, s.start_line FROM symbols s JOIN files f ON f.id = s.file_id
|
|
484
|
+
WHERE s.name IN ('main', 'Main', '__main__') AND s.kind IN ('function', 'method') LIMIT 2000`
|
|
485
|
+
)
|
|
486
|
+
.filter(e => (!scope || e.path.startsWith(prefix)) && !isTestPath(e.path) && !isVendoredPath(e.path))
|
|
487
|
+
// The real entry points sit near the top of the tree, in files named after the program.
|
|
488
|
+
.sort((a, b) => {
|
|
489
|
+
const rank = (p: string): number =>
|
|
490
|
+
p.split('/').length * 2 - (/(^|\/)(main|server|app|cli|index|program|cmd)[^/]*$/i.test(p) ? 3 : 0);
|
|
491
|
+
return rank(a.path) - rank(b.path) || a.path.localeCompare(b.path);
|
|
492
|
+
})
|
|
493
|
+
.slice(0, 8);
|
|
494
|
+
if (entries.length) {
|
|
495
|
+
out.push('entry points:');
|
|
496
|
+
for (const e of entries) out.push(` ${e.qname} ${e.path}:${e.start_line}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (scope && files.length <= 40) {
|
|
500
|
+
// Small enough to show whole: save the follow-up clast_file_summary call.
|
|
501
|
+
out.push('files:');
|
|
502
|
+
for (const f of files) {
|
|
503
|
+
const topSymbols = ctx.db.all<{ name: string }>(
|
|
504
|
+
`SELECT name FROM symbols WHERE file_id = ? AND parent_id IS NULL
|
|
505
|
+
AND kind NOT IN ('variable', 'constant', 'property', 'field')
|
|
506
|
+
ORDER BY (kind IN ('class','interface','struct','trait','enum')) DESC, exported DESC, start_line LIMIT 7`,
|
|
507
|
+
f.id
|
|
508
|
+
);
|
|
509
|
+
const names = topSymbols.slice(0, 6).map(t => t.name).join(', ');
|
|
510
|
+
out.push(` ${f.path.slice(prefix.length)} (${f.line_count} lines)${names ? ': ' + names + (topSymbols.length > 6 ? ', ...' : '') : ''}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
out.push('next: clast_file_summary <dir or file> for an outline, clast_search <words> to find symbols.');
|
|
514
|
+
const text = out.join('\n');
|
|
515
|
+
cache.set(scope, { gen: ctx.db.generation, text });
|
|
516
|
+
return note + text;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
// clast_grep
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
|
|
523
|
+
export interface GrepArgs {
|
|
524
|
+
pattern: string;
|
|
525
|
+
/** Treat pattern as a JavaScript regular expression. Default false (literal). */
|
|
526
|
+
regex?: boolean;
|
|
527
|
+
path?: string;
|
|
528
|
+
lang?: string;
|
|
529
|
+
/** Default: smart case (case-insensitive unless the pattern has uppercase). */
|
|
530
|
+
caseSensitive?: boolean;
|
|
531
|
+
/** Max matching lines. Default 50. */
|
|
532
|
+
limit?: number;
|
|
533
|
+
includeTests?: boolean;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Non-indexed text files, cached per project (re-listed at most once a minute). */
|
|
537
|
+
const otherFilesCache = new WeakMap<object, { at: number; files: string[] }>();
|
|
538
|
+
|
|
539
|
+
export function otherTextFiles(ctx: ToolContext): string[] {
|
|
540
|
+
const hit = otherFilesCache.get(ctx.indexer);
|
|
541
|
+
if (hit && Date.now() - hit.at < 60_000) return hit.files;
|
|
542
|
+
const files = ctx.indexer.rules.otherTextFiles();
|
|
543
|
+
otherFilesCache.set(ctx.indexer, { at: Date.now(), files });
|
|
544
|
+
return files;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export async function grep(ctx: ToolContext, args: GrepArgs): Promise<string> {
|
|
548
|
+
const note = await ctx.ready();
|
|
549
|
+
if (!args.pattern) return 'error: pattern is required';
|
|
550
|
+
const limit = Math.max(1, Math.min(args.limit ?? 50, 500));
|
|
551
|
+
const caseSensitive = args.caseSensitive ?? /[A-Z]/.test(args.pattern);
|
|
552
|
+
let re: RegExp;
|
|
553
|
+
try {
|
|
554
|
+
const source = args.regex ? args.pattern : args.pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
555
|
+
re = new RegExp(source, caseSensitive ? 'g' : 'gi');
|
|
556
|
+
} catch (err) {
|
|
557
|
+
return `error: invalid regular expression: ${err instanceof Error ? err.message : err}`;
|
|
558
|
+
}
|
|
559
|
+
// For plain ASCII literals a one-byte view of the file is enough to reject
|
|
560
|
+
// it, which avoids UTF-8 decoding the (vast majority of) non-matching files.
|
|
561
|
+
const cheapProbe = !args.regex && /^[\x20-\x7E]+$/.test(args.pattern);
|
|
562
|
+
const needle = cheapProbe && caseSensitive ? Buffer.from(args.pattern, 'latin1') : null;
|
|
563
|
+
|
|
564
|
+
let files = ctx.db.all<{ id: number; path: string; lang: string }>(`SELECT id, path, lang FROM files ORDER BY path`);
|
|
565
|
+
if (args.path) files = files.filter(f => pathMatches(f.path, args.path!));
|
|
566
|
+
if (args.lang) files = files.filter(f => f.lang === args.lang);
|
|
567
|
+
if (args.includeTests === false) files = files.filter(f => !isTestPath(f.path));
|
|
568
|
+
files.sort((a, b) => Number(isTestPath(a.path)) - Number(isTestPath(b.path)));
|
|
569
|
+
|
|
570
|
+
// Then everything else that is text: config, docs, data files, unsupported languages.
|
|
571
|
+
let others: string[] = [];
|
|
572
|
+
if (!args.lang) {
|
|
573
|
+
others = otherTextFiles(ctx);
|
|
574
|
+
if (args.path) others = others.filter(p => pathMatches(p, args.path!));
|
|
575
|
+
if (args.includeTests === false) others = others.filter(p => !isTestPath(p));
|
|
576
|
+
// Shallow files (package.json, pyproject.toml, Makefile) before deep trees of docs and data.
|
|
577
|
+
const depthOf = (p: string): number => p.split('/').length;
|
|
578
|
+
others.sort((a, b) => Number(isTestPath(a)) - Number(isTestPath(b)) || depthOf(a) - depthOf(b));
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const started = performance.now();
|
|
582
|
+
const out: string[] = [];
|
|
583
|
+
let matches = 0;
|
|
584
|
+
let filesWithMatches = 0;
|
|
585
|
+
let scanned = 0;
|
|
586
|
+
let timedOut = false;
|
|
587
|
+
const total = files.length + others.length;
|
|
588
|
+
|
|
589
|
+
const scanFile = async (path: string, fileId: number | null, buf: Buffer | null): Promise<void> => {
|
|
590
|
+
if (!buf) return;
|
|
591
|
+
scanned++;
|
|
592
|
+
if (fileId === null && (buf.length > ctx.clast.config.maxFileSize || buf.subarray(0, 1024).includes(0))) return;
|
|
593
|
+
if (needle) {
|
|
594
|
+
if (!buf.includes(needle)) return;
|
|
595
|
+
} else if (cheapProbe) {
|
|
596
|
+
re.lastIndex = 0;
|
|
597
|
+
if (!re.test(buf.toString('latin1'))) return;
|
|
598
|
+
}
|
|
599
|
+
let text = buf.toString('utf-8');
|
|
600
|
+
if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
|
|
601
|
+
re.lastIndex = 0;
|
|
602
|
+
if (!cheapProbe && !re.test(text)) return;
|
|
603
|
+
if (fileId !== null) await ctx.fresh([path]);
|
|
604
|
+
filesWithMatches++;
|
|
605
|
+
out.push(fileId === null ? `${path} (not indexed)` : path);
|
|
606
|
+
re.lastIndex = 0;
|
|
607
|
+
let lineNo = 1;
|
|
608
|
+
let pos = 0;
|
|
609
|
+
let lastLine = -1;
|
|
610
|
+
let m: RegExpExecArray | null;
|
|
611
|
+
while ((m = re.exec(text)) !== null) {
|
|
612
|
+
if (m[0].length === 0) { re.lastIndex++; continue; }
|
|
613
|
+
for (let i = text.indexOf('\n', pos); i !== -1 && i < m.index; i = text.indexOf('\n', i + 1)) {
|
|
614
|
+
lineNo++;
|
|
615
|
+
pos = i + 1;
|
|
616
|
+
}
|
|
617
|
+
if (lineNo === lastLine) continue;
|
|
618
|
+
lastLine = lineNo;
|
|
619
|
+
if (matches >= limit) {
|
|
620
|
+
out.push(' ... more matches in this file');
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
const end = text.indexOf('\n', m.index);
|
|
624
|
+
let line = text.slice(pos, end === -1 ? text.length : end).trim();
|
|
625
|
+
if (line.length > 170) {
|
|
626
|
+
// Keep the match visible in very long lines.
|
|
627
|
+
const at = Math.max(0, m.index - pos - 60);
|
|
628
|
+
line = (at > 0 ? '...' : '') + line.slice(at, at + 165) + '...';
|
|
629
|
+
}
|
|
630
|
+
const enclosing = fileId !== null ? ctx.db.enclosingSymbol(fileId, lineNo) : undefined;
|
|
631
|
+
out.push(` ${String(lineNo).padStart(5)} ${enclosing ? '[' + enclosing.qname + '] ' : ''}${line}`);
|
|
632
|
+
matches++;
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// Read in parallel batches (file opens dominate on large trees), process in order.
|
|
637
|
+
const work: Array<{ path: string; id: number | null }> = [
|
|
638
|
+
...files.map(f => ({ path: f.path, id: f.id as number | null })),
|
|
639
|
+
...others.map(p => ({ path: p, id: null })),
|
|
640
|
+
];
|
|
641
|
+
const BATCH = 96;
|
|
642
|
+
for (let i = 0; i < work.length && matches < limit; i += BATCH) {
|
|
643
|
+
if (performance.now() - started > 8000) { timedOut = true; break; }
|
|
644
|
+
const batch = work.slice(i, i + BATCH);
|
|
645
|
+
const bufs = await Promise.all(
|
|
646
|
+
batch.map(w => readFile(join(ctx.clast.root, w.path)).catch(() => null))
|
|
647
|
+
);
|
|
648
|
+
for (let k = 0; k < batch.length && matches < limit; k++) {
|
|
649
|
+
await scanFile(batch[k]!.path, batch[k]!.id, bufs[k] ?? null);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const what = args.regex ? '/' + args.pattern + '/' : JSON.stringify(args.pattern);
|
|
654
|
+
const header =
|
|
655
|
+
matches === 0
|
|
656
|
+
? `no matches for ${what} in ${plural(scanned, 'file')}` +
|
|
657
|
+
(timedOut ? ' (time budget hit before all files were read; narrow with path)' : '') +
|
|
658
|
+
(caseSensitive && !args.caseSensitive ? ' (case-sensitive because the pattern has uppercase; pass caseSensitive=false to relax)' : '')
|
|
659
|
+
: `${matches >= limit ? 'first ' : ''}${plural(matches, 'matching line')} in ${plural(filesWithMatches, 'file')}` +
|
|
660
|
+
(matches >= limit ? ` (limit reached after ${scanned}/${total} files; narrow with path or raise limit)` : '') +
|
|
661
|
+
(timedOut ? ' (time budget hit; narrow with path)' : '') +
|
|
662
|
+
' [enclosing symbol in brackets]';
|
|
663
|
+
return note + [header, ...out].join('\n');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ---------------------------------------------------------------------------
|
|
667
|
+
// clast_changes
|
|
668
|
+
// ---------------------------------------------------------------------------
|
|
669
|
+
|
|
670
|
+
export interface ChangesArgs {
|
|
671
|
+
/** Git ref to diff against. Default HEAD (uncommitted changes). */
|
|
672
|
+
base?: string;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function git(root: string, args: string[]): string | null {
|
|
676
|
+
try {
|
|
677
|
+
return execFileSync('git', args, {
|
|
678
|
+
cwd: root, encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
|
|
679
|
+
});
|
|
680
|
+
} catch {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
export async function changes(ctx: ToolContext, args: ChangesArgs = {}): Promise<string> {
|
|
686
|
+
const note = await ctx.ready();
|
|
687
|
+
const root = ctx.clast.root;
|
|
688
|
+
const base = args.base?.trim() || 'HEAD';
|
|
689
|
+
if (!/^[\w./~^@{}-]+$/.test(base)) return 'error: invalid base ref';
|
|
690
|
+
|
|
691
|
+
if (git(root, ['rev-parse', '--git-dir']) === null) {
|
|
692
|
+
return note + 'error: this project is not a git repository (or git is not installed); clast_changes needs git.';
|
|
693
|
+
}
|
|
694
|
+
if (git(root, ['rev-parse', '--verify', '--quiet', base + '^{commit}']) === null) {
|
|
695
|
+
return note + `error: git ref "${base}" does not exist in this repository (shallow clone or typo?). Try HEAD, a branch name, or a commit hash.`;
|
|
696
|
+
}
|
|
697
|
+
const diff = git(root, ['diff', '--unified=0', '--no-color', '--no-ext-diff', '--relative', base, '--']);
|
|
698
|
+
if (diff === null) return note + `error: git diff against "${base}" failed.`;
|
|
699
|
+
|
|
700
|
+
// file -> changed line ranges on the new side
|
|
701
|
+
const changed = new Map<string, Array<[number, number]>>();
|
|
702
|
+
const stats = new Map<string, { add: number; del: number }>();
|
|
703
|
+
let current: string | null = null;
|
|
704
|
+
for (const line of diff.split('\n')) {
|
|
705
|
+
if (line.startsWith('+++ ')) {
|
|
706
|
+
const p = line.slice(4).trim();
|
|
707
|
+
current = p === '/dev/null' ? null : p.replace(/^b\//, '');
|
|
708
|
+
if (current && !changed.has(current)) {
|
|
709
|
+
changed.set(current, []);
|
|
710
|
+
stats.set(current, { add: 0, del: 0 });
|
|
711
|
+
}
|
|
712
|
+
} else if (line.startsWith('@@') && current) {
|
|
713
|
+
const m = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
|
|
714
|
+
if (!m) continue;
|
|
715
|
+
const del = m[2] === undefined ? 1 : Number(m[2]);
|
|
716
|
+
const start = Number(m[3]);
|
|
717
|
+
const count = m[4] === undefined ? 1 : Number(m[4]);
|
|
718
|
+
const st = stats.get(current)!;
|
|
719
|
+
st.add += count;
|
|
720
|
+
st.del += del;
|
|
721
|
+
changed.get(current)!.push(count === 0 ? [start, start + 1] : [start, start + count - 1]);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
// Untracked files are new code too.
|
|
725
|
+
const untracked = git(root, ['ls-files', '--others', '--exclude-standard']) ?? '';
|
|
726
|
+
for (const p of untracked.split('\n').map(s => s.trim()).filter(Boolean)) {
|
|
727
|
+
if (!changed.has(p) && ctx.indexer.rules.isSupported(p)) {
|
|
728
|
+
changed.set(p, [[1, Number.MAX_SAFE_INTEGER]]);
|
|
729
|
+
stats.set(p, { add: -1, del: 0 });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (changed.size === 0) return note + `no changes relative to ${base}.`;
|
|
734
|
+
await ctx.fresh(changed.keys());
|
|
735
|
+
|
|
736
|
+
const out: string[] = [];
|
|
737
|
+
const touched: SymbolRow[] = [];
|
|
738
|
+
let unindexed = 0;
|
|
739
|
+
for (const [path, ranges] of changed) {
|
|
740
|
+
const file = ctx.db.fileByPath(path);
|
|
741
|
+
const st = stats.get(path)!;
|
|
742
|
+
const stat = st.add === -1 ? 'new file' : `+${st.add} -${st.del}`;
|
|
743
|
+
if (!file) {
|
|
744
|
+
unindexed++;
|
|
745
|
+
out.push(`${path} (${stat}; not an indexed source file)`);
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
const symbols = ctx.db.symbolsInFile(file.id);
|
|
749
|
+
// Innermost symbols overlapping a changed range; containers only when no member covers the change.
|
|
750
|
+
const hit = new Set<number>();
|
|
751
|
+
for (const [a, b] of ranges) {
|
|
752
|
+
const overlapping = symbols.filter(s => s.start_line <= b && s.end_line >= a);
|
|
753
|
+
for (const s of overlapping) {
|
|
754
|
+
const hasInner = overlapping.some(o => o.parent_id === s.id);
|
|
755
|
+
if (CONTAINER_KINDS.has(s.kind) && hasInner) continue;
|
|
756
|
+
hit.add(s.id);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
out.push(`${path} (${stat})`);
|
|
760
|
+
const list = symbols.filter(s => hit.has(s.id));
|
|
761
|
+
for (const s of list.slice(0, 40)) {
|
|
762
|
+
out.push(` ${s.kind} ${s.qname} :${range(s)}`);
|
|
763
|
+
touched.push(s);
|
|
764
|
+
}
|
|
765
|
+
if (list.length > 40) out.push(` ... ${list.length - 40} more symbols`);
|
|
766
|
+
if (list.length === 0) out.push(' (changes outside any symbol: imports, top-level statements or comments)');
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Impact: who calls the changed callables, and which tests exercise them.
|
|
770
|
+
const impact: string[] = [];
|
|
771
|
+
const tests = new Set<string>();
|
|
772
|
+
const changedIds = new Set(touched.map(t => t.id));
|
|
773
|
+
for (const s of touched) {
|
|
774
|
+
if (s.kind !== 'function' && s.kind !== 'method' && s.kind !== 'class') continue;
|
|
775
|
+
if (impact.length >= 40) break;
|
|
776
|
+
const callers = callersOf(ctx.db, ctx.resolver, s).filter(c => !c.caller || !changedIds.has(c.caller.id));
|
|
777
|
+
if (callers.length === 0) continue;
|
|
778
|
+
const prod: string[] = [];
|
|
779
|
+
for (const c of callers) {
|
|
780
|
+
if (isTestPath(c.path)) tests.add(c.path);
|
|
781
|
+
else if (prod.length < 5) prod.push(`${c.caller ? c.caller.qname : c.path}${c.certain ? '' : '?'}`);
|
|
782
|
+
}
|
|
783
|
+
const prodCount = callers.filter(c => !isTestPath(c.path)).length;
|
|
784
|
+
if (prodCount) {
|
|
785
|
+
impact.push(` ${s.qname} <- ${[...new Set(prod)].join(', ')}${prodCount > prod.length ? `, +${prodCount - prod.length} more` : ''}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
const header = `changes vs ${base}: ${plural(changed.size, 'file')}, ${plural(touched.length, 'symbol')} touched` +
|
|
789
|
+
(unindexed ? ` (${unindexed} not indexed)` : '');
|
|
790
|
+
const result = [header, ...out];
|
|
791
|
+
if (impact.length) {
|
|
792
|
+
result.push('callers outside the change (review for breakage):');
|
|
793
|
+
result.push(...impact);
|
|
794
|
+
}
|
|
795
|
+
if (tests.size) {
|
|
796
|
+
result.push(`tests exercising changed symbols (${tests.size}):`);
|
|
797
|
+
for (const t of [...tests].slice(0, 15)) result.push(` ${t}`);
|
|
798
|
+
if (tests.size > 15) result.push(` ... ${tests.size - 15} more`);
|
|
799
|
+
}
|
|
800
|
+
return note + result.join('\n');
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// ---------------------------------------------------------------------------
|
|
804
|
+
// clast_status / clast_reindex
|
|
805
|
+
// ---------------------------------------------------------------------------
|
|
806
|
+
|
|
807
|
+
export async function status(ctx: ToolContext, watcher?: { isActive: boolean; pendingCount: number }): Promise<string> {
|
|
808
|
+
const s = ctx.db.stats();
|
|
809
|
+
const p = ctx.indexer.progress;
|
|
810
|
+
const out: string[] = [];
|
|
811
|
+
out.push(`project: ${ctx.clast.root}`);
|
|
812
|
+
out.push(
|
|
813
|
+
`index: ${plural(s.files, 'file')}, ${s.symbols} symbols, ${s.calls} call edges, ${s.names} distinct identifiers, ${s.imports} imports`
|
|
814
|
+
);
|
|
815
|
+
const langs = ctx.db.all<{ lang: string; n: number }>(`SELECT lang, COUNT(*) AS n FROM files GROUP BY lang ORDER BY n DESC`);
|
|
816
|
+
if (langs.length) out.push('languages: ' + langs.map(l => `${l.lang} ${l.n}`).join(', '));
|
|
817
|
+
out.push(
|
|
818
|
+
ctx.indexer.isIndexing
|
|
819
|
+
? `state: indexing (${p.phase}, ${p.done}/${p.total} files)`
|
|
820
|
+
: 'state: idle (up to date as of last scan; files are also re-checked when queried)'
|
|
821
|
+
);
|
|
822
|
+
if (ctx.indexer.lastResult) {
|
|
823
|
+
const r = ctx.indexer.lastResult;
|
|
824
|
+
out.push(
|
|
825
|
+
`last index run: scanned ${r.scanned}, parsed ${r.parsed}, removed ${r.removed} in ${r.durationMs}ms (${plural(r.workers, 'worker')})`
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
if (s.lastIndexed) out.push(`last file indexed: ${new Date(s.lastIndexed).toISOString()}`);
|
|
829
|
+
if (ctx.indexer.rules.skippedLarge) {
|
|
830
|
+
out.push(`skipped: ${plural(ctx.indexer.rules.skippedLarge, 'file')} larger than ${ctx.clast.config.maxFileSize} bytes (maxFileSize)`);
|
|
831
|
+
}
|
|
832
|
+
if (ctx.indexer.rules.truncated) {
|
|
833
|
+
out.push(
|
|
834
|
+
`warning: stopped at ${ctx.clast.config.maxFiles} source files (maxFiles). Is the project root too broad? ` +
|
|
835
|
+
`Set CLAST_PROJECT_DIR or raise maxFiles in clast.config.json.`
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
if (watcher) out.push(`watcher: ${watcher.isActive ? 'active' : 'off'}${watcher.pendingCount ? `, ${watcher.pendingCount} pending` : ''}`);
|
|
839
|
+
out.push(`database: ${ctx.db.path}`);
|
|
840
|
+
return out.join('\n');
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
export async function reindex(ctx: ToolContext, args: { path?: string; force?: boolean } = {}): Promise<string> {
|
|
844
|
+
if (args.path) {
|
|
845
|
+
const rel = ctx.indexer.toRel(args.path);
|
|
846
|
+
const changed = await ctx.indexer.indexFile(rel, { force: true });
|
|
847
|
+
ctx.lines.invalidate(rel);
|
|
848
|
+
const row = ctx.db.fileByPath(rel);
|
|
849
|
+
return row
|
|
850
|
+
? `reindexed ${rel}: ${plural(row.symbol_count, 'symbol')}${changed ? '' : ' (no change)'}`
|
|
851
|
+
: `${rel} is not indexable (unsupported type, ignored, too large, or missing)`;
|
|
852
|
+
}
|
|
853
|
+
const r = await ctx.indexer.indexAll({ force: args.force ?? false });
|
|
854
|
+
ctx.lines.invalidate();
|
|
855
|
+
const s = ctx.db.stats();
|
|
856
|
+
return (
|
|
857
|
+
`${args.force ? 'full rebuild' : 'incremental scan'}: scanned ${r.scanned} files, parsed ${r.parsed}, removed ${r.removed} ` +
|
|
858
|
+
`in ${r.durationMs}ms. Index: ${s.files} files, ${s.symbols} symbols.`
|
|
859
|
+
);
|
|
860
|
+
}
|