clast-ai 0.1.2 → 0.2.1
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 +468 -398
- 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 +266 -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/migrate.d.ts +35 -0
- package/dist/migrate.js +194 -0
- package/dist/migrate.js.map +1 -0
- 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 +72 -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 +265 -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/migrate.ts +213 -0
- 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 +6 -3
- 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,843 @@
|
|
|
1
|
+
import { SYMBOL_COLUMNS, SYMBOL_FROM } from '../db/index.js';
|
|
2
|
+
import { CALLABLE_KINDS, CONTAINER_KINDS, SYMBOL_KINDS } from '../types.js';
|
|
3
|
+
import { buildGraph, calleesOf, callersOf, externalCallSites, groupCallers, } from '../query/callgraph.js';
|
|
4
|
+
import { firstLine, isTestPath, isTestSite, loc, plural, symLine } from '../query/format.js';
|
|
5
|
+
import { directSubtypes, directSupertypes, structuralImplementers, structuralInterfaces, } from '../query/hierarchy.js';
|
|
6
|
+
import { findSymbols, isFileLocal, otherDefinitions, parseSymbolRef, pathMatches } from '../query/resolve.js';
|
|
7
|
+
import { searchSymbols } from '../query/search.js';
|
|
8
|
+
import { toList } from './context.js';
|
|
9
|
+
export async function search(ctx, args) {
|
|
10
|
+
const note = await ctx.ready();
|
|
11
|
+
const queries = toList(args.query);
|
|
12
|
+
if (queries.length === 0)
|
|
13
|
+
return 'error: query is required';
|
|
14
|
+
const kinds = args.kind ? (Array.isArray(args.kind) ? args.kind : [args.kind]) : [];
|
|
15
|
+
const badKind = kinds.find(k => !SYMBOL_KINDS.includes(k));
|
|
16
|
+
if (badKind)
|
|
17
|
+
return `error: unknown kind "${badKind}". Valid kinds: ${SYMBOL_KINDS.join(', ')}.`;
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const q of queries) {
|
|
20
|
+
const { hits, truncated } = searchSymbols(ctx.db, q, args);
|
|
21
|
+
if (hits.length === 0) {
|
|
22
|
+
out.push(`"${q}": no symbols found. The name may be defined dynamically or outside the indexed code: ` +
|
|
23
|
+
`try clast_grep for the text, or fewer / different words.`);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
out.push(`"${q}": ${plural(hits.length, 'match')}${truncated ? ' (more exist; raise limit or add kind/path filters)' : ''}`);
|
|
27
|
+
for (const h of hits) {
|
|
28
|
+
const s = h.sym;
|
|
29
|
+
const tags = [];
|
|
30
|
+
if (h.refs > 1 && h.unique)
|
|
31
|
+
tags.push(`refs:${h.refs}`);
|
|
32
|
+
if (h.match !== 'exact')
|
|
33
|
+
tags.push(h.match);
|
|
34
|
+
if (h.overloads)
|
|
35
|
+
tags.push(`+${plural(h.overloads, 'overload')}`);
|
|
36
|
+
out.push(`${symLine(s)}${tags.length ? ' [' + tags.join(' ') + ']' : ''}`);
|
|
37
|
+
if (s.signature && s.signature !== s.name)
|
|
38
|
+
out.push(` ${firstLine(s.signature, 180)}`);
|
|
39
|
+
if (s.doc)
|
|
40
|
+
out.push(` // ${firstLine(s.doc, 140)}`);
|
|
41
|
+
}
|
|
42
|
+
if (hits.some(h => h.match === 'body')) {
|
|
43
|
+
out.push('([body] = matched by what the function\'s code mentions, not by its name or documentation)');
|
|
44
|
+
}
|
|
45
|
+
out.push('');
|
|
46
|
+
}
|
|
47
|
+
return note + out.join('\n').trimEnd();
|
|
48
|
+
}
|
|
49
|
+
const MAX_DOC_LINES = 12;
|
|
50
|
+
const IMPORT_LINE = /^(import\b|from\b.*\bimport\b|export\s*(\*|\{)|use\s|using\s|#\s*include|require(_relative|_once)?\b|include(_once)?\b|(const|let|var)\s.*=\s*require\()/;
|
|
51
|
+
const MINOR_KINDS = new Set(['property', 'field', 'constant', 'variable']);
|
|
52
|
+
function range(s) {
|
|
53
|
+
return s.start_line === s.end_line ? `${s.start_line}` : `${s.start_line}-${s.end_line}`;
|
|
54
|
+
}
|
|
55
|
+
/** Members of a container: callables with signatures first, data members as a compact name list. */
|
|
56
|
+
function memberOutline(ctx, sym, max) {
|
|
57
|
+
const members = ctx.db.symbolsInFile(sym.file_id).filter(m => m.parent_id === sym.id);
|
|
58
|
+
const callables = members.filter(m => !MINOR_KINDS.has(m.kind));
|
|
59
|
+
const data = members.filter(m => MINOR_KINDS.has(m.kind));
|
|
60
|
+
const out = [];
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
let shown = 0;
|
|
63
|
+
for (const m of callables) {
|
|
64
|
+
if (shown >= max)
|
|
65
|
+
break;
|
|
66
|
+
const line = ` ${range(m).padEnd(9)} ${m.signature || m.kind + ' ' + m.name}${m.doc ? ' // ' + firstLine(m.doc, 80) : ''}`;
|
|
67
|
+
if (seen.has(line))
|
|
68
|
+
continue;
|
|
69
|
+
seen.add(line);
|
|
70
|
+
out.push(line);
|
|
71
|
+
shown++;
|
|
72
|
+
}
|
|
73
|
+
if (callables.length > shown)
|
|
74
|
+
out.push(` ... ${callables.length - shown} more methods (clast_file_summary ${sym.path} lists all)`);
|
|
75
|
+
if (data.length) {
|
|
76
|
+
const names = [...new Set(data.map(d => d.name))];
|
|
77
|
+
out.push(` fields (${names.length}): ${names.slice(0, 40).join(', ')}${names.length > 40 ? ', ...' : ''}`);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
/** Languages where methods are (or may be) written outside the body of their type. */
|
|
82
|
+
const OUTSIDE_METHOD_LANGS = new Set(['go', 'rust', 'cpp', 'c', 'swift', 'dart', 'csharp']);
|
|
83
|
+
/**
|
|
84
|
+
* Methods of a type that are defined outside its body: Go receivers, Rust impl
|
|
85
|
+
* blocks, C++ out-of-line definitions, Swift / Dart extensions, other parts of
|
|
86
|
+
* a C# partial class. A struct's source alone does not show them.
|
|
87
|
+
*/
|
|
88
|
+
function outsideMethods(ctx, sym, max) {
|
|
89
|
+
if (!OUTSIDE_METHOD_LANGS.has(sym.lang))
|
|
90
|
+
return [];
|
|
91
|
+
const prefix = sym.qname + '.';
|
|
92
|
+
const dir = sym.path.slice(0, sym.path.lastIndexOf('/') + 1);
|
|
93
|
+
const rows = ctx.db
|
|
94
|
+
.all(`SELECT ${SYMBOL_COLUMNS} FROM ${SYMBOL_FROM}
|
|
95
|
+
WHERE s.qname LIKE ? ESCAPE '\\' AND s.kind IN ('method', 'function') AND f.lang = ? LIMIT 2000`, prefix.replace(/[\\%_]/g, c => '\\' + c) + '%', sym.lang)
|
|
96
|
+
.filter(m => m.parent_id !== sym.id && m.qname.startsWith(prefix) && !m.qname.slice(prefix.length).includes('.') &&
|
|
97
|
+
// Go type names repeat across packages: stay with this one's directory. Rust modules are files:
|
|
98
|
+
// a same-named type next door (semaphore.rs / batch_semaphore.rs) is another type.
|
|
99
|
+
(sym.lang === 'go' ? m.path.startsWith(dir) && !m.path.slice(dir.length).includes('/') : sym.lang === 'rust' ? m.file_id === sym.file_id : true));
|
|
100
|
+
if (rows.length === 0)
|
|
101
|
+
return [];
|
|
102
|
+
rows.sort((a, b) => a.path.localeCompare(b.path) || a.start_line - b.start_line);
|
|
103
|
+
const out = [`methods defined outside the body (${rows.length}):`];
|
|
104
|
+
let file = '';
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
let shown = 0;
|
|
107
|
+
for (const m of rows) {
|
|
108
|
+
if (shown >= max)
|
|
109
|
+
break;
|
|
110
|
+
const key = m.path + ':' + m.name;
|
|
111
|
+
if (seen.has(key))
|
|
112
|
+
continue; // overloads / cfg variants
|
|
113
|
+
seen.add(key);
|
|
114
|
+
if (m.path !== file) {
|
|
115
|
+
file = m.path;
|
|
116
|
+
out.push(` ${file}`);
|
|
117
|
+
}
|
|
118
|
+
out.push(` ${range(m).padEnd(9)} ${firstLine(m.signature || m.name, 130)}`);
|
|
119
|
+
shown++;
|
|
120
|
+
}
|
|
121
|
+
if (rows.length > shown && shown >= max)
|
|
122
|
+
out.push(` ... more (clast_search "${sym.name}." or clast_file_summary on the files above)`);
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
/** Doc comment lines for signature-only output, capped. */
|
|
126
|
+
function docLines(doc, max) {
|
|
127
|
+
const lines = doc.split('\n');
|
|
128
|
+
const out = lines.slice(0, max).map(l => '// ' + l);
|
|
129
|
+
if (lines.length > max)
|
|
130
|
+
out.push(`// ... (${lines.length - max} more doc lines)`);
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
function callerLines(edges, out) {
|
|
134
|
+
const groups = groupCallers(edges);
|
|
135
|
+
if (groups.length === 0)
|
|
136
|
+
return;
|
|
137
|
+
const prod = groups.filter(g => !isTestSite(g.path, g.caller?.qname));
|
|
138
|
+
const tests = groups.filter(g => isTestSite(g.path, g.caller?.qname));
|
|
139
|
+
const certain = prod.filter(g => g.certain);
|
|
140
|
+
const possible = prod.filter(g => !g.certain && !g.weak);
|
|
141
|
+
const unknown = prod.filter(g => g.weak);
|
|
142
|
+
const parts = [];
|
|
143
|
+
if (certain.length)
|
|
144
|
+
parts.push(`${certain.length} resolved`);
|
|
145
|
+
if (possible.length)
|
|
146
|
+
parts.push(`${possible.length} name-only "?"`);
|
|
147
|
+
if (unknown.length)
|
|
148
|
+
parts.push(`${unknown.length} on receivers of unknown type`);
|
|
149
|
+
if (tests.length)
|
|
150
|
+
parts.push(`${tests.length} in tests/examples`);
|
|
151
|
+
out.push(`callers (${parts.join(', ')}):`);
|
|
152
|
+
const line = (g, mark) => ` ${g.caller ? g.caller.qname : '<file level>'} ${g.path}:${g.line}${g.sites > 1 ? ` (x${g.sites})` : ''}${mark}`;
|
|
153
|
+
// Resolved production callers are what matters; weaker matches fill in when there are few.
|
|
154
|
+
for (const g of certain.slice(0, 10))
|
|
155
|
+
out.push(line(g, ''));
|
|
156
|
+
if (certain.length > 10)
|
|
157
|
+
out.push(` ... ${certain.length - 10} more resolved (clast_call_graph lists all)`);
|
|
158
|
+
let budget = Math.max(0, 6 - certain.length);
|
|
159
|
+
for (const g of possible.slice(0, budget))
|
|
160
|
+
out.push(line(g, ' ?'));
|
|
161
|
+
budget -= Math.min(budget, possible.length);
|
|
162
|
+
if (unknown.length) {
|
|
163
|
+
// These call a method with this name on an object whose type could not be determined.
|
|
164
|
+
for (const g of unknown.slice(0, budget))
|
|
165
|
+
out.push(line(g, ' ? (receiver type unknown)'));
|
|
166
|
+
}
|
|
167
|
+
const hidden = Math.max(0, possible.length - Math.max(0, 6 - certain.length)) + Math.max(0, unknown.length - budget);
|
|
168
|
+
if (hidden)
|
|
169
|
+
out.push(` ... ${hidden} more unconfirmed call sites (clast_call_graph with uncertain=true lists them)`);
|
|
170
|
+
if (tests.length) {
|
|
171
|
+
const files = [...new Set(tests.map(t => t.path))];
|
|
172
|
+
out.push(` tests/examples: ${files.slice(0, 4).join(', ')}${files.length > 4 ? `, +${files.length - 4} more files` : ''}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Languages where a member can be named without an object in front (implicit this / self). */
|
|
176
|
+
const IMPLICIT_MEMBER_LANGS = new Set(['java', 'csharp', 'cpp', 'swift', 'dart', 'kotlin', 'ruby']);
|
|
177
|
+
/** Places that mention the symbol without calling it (passed as a value, exported, decorated, typed). */
|
|
178
|
+
function valueReferences(ctx, sym, callers, max, accept) {
|
|
179
|
+
const nameId = ctx.db.nameId(sym.name, false);
|
|
180
|
+
if (nameId === undefined)
|
|
181
|
+
return [];
|
|
182
|
+
let rows = ctx.db.all(`SELECT o.file_id, f.path, o.lines FROM occ o JOIN files f ON f.id = o.file_id WHERE o.name_id = ? LIMIT 400`, nameId);
|
|
183
|
+
let nearest = false;
|
|
184
|
+
if (accept)
|
|
185
|
+
rows = rows.filter(r => accept(r.path));
|
|
186
|
+
// A C static function cannot be named from another file.
|
|
187
|
+
if (isFileLocal(sym))
|
|
188
|
+
rows = rows.filter(r => r.file_id === sym.file_id);
|
|
189
|
+
if (rows.length > 40) {
|
|
190
|
+
// A widely used name: only the defining file and the files importing it can mean this symbol.
|
|
191
|
+
const visible = new Set([sym.file_id]);
|
|
192
|
+
for (const r of ctx.db.all(`SELECT DISTINCT file_id FROM imports WHERE resolved_id = ? LIMIT 200`, sym.file_id)) {
|
|
193
|
+
visible.add(r.file_id);
|
|
194
|
+
}
|
|
195
|
+
const narrowed = ctx.db.all(`SELECT o.file_id, f.path, o.lines FROM occ o JOIN files f ON f.id = o.file_id WHERE o.name_id = ?`, nameId).filter(r => visible.has(r.file_id));
|
|
196
|
+
const kept = accept ? narrowed.filter(r => accept(r.path)) : narrowed;
|
|
197
|
+
// Too many files can see it to list them all (a field of a central struct): show the nearest ones.
|
|
198
|
+
if (kept.length > 40) {
|
|
199
|
+
rows.length = 0;
|
|
200
|
+
rows.push(...kept);
|
|
201
|
+
}
|
|
202
|
+
if (kept.length > 40 || !kept.some(r => r.file_id !== sym.file_id)) {
|
|
203
|
+
// No file-level imports to narrow by (C# / Java namespaces, Ruby autoload): show the nearest uses.
|
|
204
|
+
const home = sym.path.split('/');
|
|
205
|
+
const near = (p) => {
|
|
206
|
+
const segs = p.split('/');
|
|
207
|
+
let n = 0;
|
|
208
|
+
while (n < segs.length - 1 && n < home.length - 1 && segs[n] === home[n])
|
|
209
|
+
n++;
|
|
210
|
+
return n;
|
|
211
|
+
};
|
|
212
|
+
rows.sort((a, b) => Number(isTestPath(a.path)) - Number(isTestPath(b.path)) || near(b.path) - near(a.path) || a.path.localeCompare(b.path));
|
|
213
|
+
rows.length = Math.min(rows.length, 40);
|
|
214
|
+
nearest = true;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
rows.length = 0;
|
|
218
|
+
rows.push(...kept);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Every line that calls *something* with this name is a call, whichever definition it resolves to.
|
|
222
|
+
const callSites = new Set(callers.map(c => c.path + ':' + c.line));
|
|
223
|
+
for (const row of ctx.db.all(`SELECT f.path, c.line FROM calls c JOIN files f ON f.id = c.file_id WHERE c.name_id = ? LIMIT 5000`, nameId)) {
|
|
224
|
+
callSites.add(row.path + ':' + row.line);
|
|
225
|
+
}
|
|
226
|
+
const callerIds = new Set(callers.map(c => c.caller?.id).filter((id) => id !== undefined));
|
|
227
|
+
const defLines = new Set(ctx.db.symbolsByName(sym.name).map(d => d.path + ':' + d.start_line));
|
|
228
|
+
const out = [];
|
|
229
|
+
const seen = new Set();
|
|
230
|
+
const escaped = sym.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
231
|
+
const callPattern = new RegExp('\\b' + escaped + '\\s*(<[^>]*>)?[!?]?\\(');
|
|
232
|
+
// For a member (property, field, method): `ordered=True` as a keyword argument or `headers = {}` as a
|
|
233
|
+
// local are other things that happen to share the name. A use of the member has an object in front.
|
|
234
|
+
const isMember = sym.qname !== sym.name;
|
|
235
|
+
const memberUse = new RegExp('(\\.|->|::)\\s*' + escaped + '\\b');
|
|
236
|
+
const plainAssign = new RegExp('(^|[^.\\w>:$@])' + escaped + '\\s*=(?!=)');
|
|
237
|
+
if (!nearest)
|
|
238
|
+
rows.sort((a, b) => Number(isTestPath(a.path)) - Number(isTestPath(b.path)) || a.path.localeCompare(b.path));
|
|
239
|
+
for (const r of rows) {
|
|
240
|
+
for (const n of r.lines.split(',').map(Number)) {
|
|
241
|
+
const key = r.path + ':' + n;
|
|
242
|
+
if (callSites.has(key) || defLines.has(key))
|
|
243
|
+
continue;
|
|
244
|
+
if (r.file_id === sym.file_id && n >= sym.start_line && n <= sym.end_line)
|
|
245
|
+
continue;
|
|
246
|
+
const enclosing = ctx.db.enclosingSymbol(r.file_id, n);
|
|
247
|
+
if (enclosing && callerIds.has(enclosing.id))
|
|
248
|
+
continue;
|
|
249
|
+
const text = ctx.lines.line(r.path, n, 90);
|
|
250
|
+
// Imports only say the name is in scope; they are not uses.
|
|
251
|
+
if (!enclosing && IMPORT_LINE.test(text))
|
|
252
|
+
continue;
|
|
253
|
+
// The line calls the name (a repeat call inside a caller already listed, or a call on an untyped receiver).
|
|
254
|
+
if (callPattern.test(text))
|
|
255
|
+
continue;
|
|
256
|
+
if (isMember && r.file_id !== sym.file_id && plainAssign.test(text) && !memberUse.test(text))
|
|
257
|
+
continue;
|
|
258
|
+
// Without an implicit `this`, a bare `ordered` is a local variable, not the member `qs.ordered`.
|
|
259
|
+
if (isMember && !IMPLICIT_MEMBER_LANGS.has(sym.lang) && !memberUse.test(text))
|
|
260
|
+
continue;
|
|
261
|
+
const who = enclosing ? enclosing.qname : '<file level>';
|
|
262
|
+
if (seen.has(r.path + who))
|
|
263
|
+
continue;
|
|
264
|
+
seen.add(r.path + who);
|
|
265
|
+
if (out.length < max)
|
|
266
|
+
out.push(` ${who} ${key} ${text}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (out.length === 0)
|
|
270
|
+
return [];
|
|
271
|
+
if (seen.size > out.length || nearest) {
|
|
272
|
+
out.push(` ... ${nearest ? 'more' : seen.size - out.length + ' more'} (clast_references ${sym.name}${nearest ? ' lists every use; nearest shown' : ''})`);
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
function subtypeLines(ctx, sym, max, out) {
|
|
277
|
+
const subs = directSubtypes(ctx.db, ctx.resolver, sym);
|
|
278
|
+
const structural = structuralImplementers(ctx.db, sym, 60);
|
|
279
|
+
if (subs.length) {
|
|
280
|
+
out.push(`subtypes (${subs.length} direct):`);
|
|
281
|
+
for (const s of subs.slice(0, max))
|
|
282
|
+
out.push(` ${s.sym.qname} ${s.sym.path}:${s.sym.start_line}${s.certain ? '' : ' ?'}`);
|
|
283
|
+
if (subs.length > max)
|
|
284
|
+
out.push(` ... ${subs.length - max} more (clast_hierarchy ${sym.qname} lists all, transitively)`);
|
|
285
|
+
}
|
|
286
|
+
if (structural.length) {
|
|
287
|
+
out.push(`implemented by (${structural.length}, matched by method set):`);
|
|
288
|
+
for (const s of structural.slice(0, max))
|
|
289
|
+
out.push(` ${s.qname} ${s.path}:${s.start_line}`);
|
|
290
|
+
if (structural.length > max)
|
|
291
|
+
out.push(` ... ${structural.length - max} more (clast_hierarchy)`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async function describe(ctx, sym, args, overloads) {
|
|
295
|
+
const out = [];
|
|
296
|
+
const maxLines = Math.max(1, Math.min(args.maxLines ?? 120, 2000));
|
|
297
|
+
out.push(`${symLine(sym)}${sym.exported ? ' [exported]' : ''}`);
|
|
298
|
+
const isContainer = CONTAINER_KINDS.has(sym.kind);
|
|
299
|
+
const hasDoc = sym.doc_line > 0 && sym.doc_line < sym.start_line;
|
|
300
|
+
const codeLines = sym.end_line - sym.start_line + 1;
|
|
301
|
+
if (args.includeBody === false) {
|
|
302
|
+
if (sym.signature)
|
|
303
|
+
out.push(firstLine(sym.signature, 200));
|
|
304
|
+
if (sym.doc)
|
|
305
|
+
out.push(...docLines(sym.doc, 6));
|
|
306
|
+
if (isContainer) {
|
|
307
|
+
const members = ctx.db.symbolsInFile(sym.file_id).filter(m => m.parent_id === sym.id);
|
|
308
|
+
const methods = [...new Set(members.filter(m => !MINOR_KINDS.has(m.kind)).map(m => m.name))];
|
|
309
|
+
const data = [...new Set(members.filter(m => MINOR_KINDS.has(m.kind)).map(m => m.name))];
|
|
310
|
+
if (methods.length)
|
|
311
|
+
out.push(`methods (${methods.length}): ${methods.slice(0, 50).join(', ')}${methods.length > 50 ? ', ...' : ''}`);
|
|
312
|
+
if (data.length)
|
|
313
|
+
out.push(`fields (${data.length}): ${data.slice(0, 30).join(', ')}${data.length > 30 ? ', ...' : ''}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else if (isContainer && codeLines > maxLines) {
|
|
317
|
+
// Large container: signature + doc + member outline instead of a wall of code.
|
|
318
|
+
if (sym.signature)
|
|
319
|
+
out.push(sym.signature);
|
|
320
|
+
if (sym.doc)
|
|
321
|
+
out.push(...docLines(sym.doc, 6));
|
|
322
|
+
out.push(`members (${codeLines} lines; ask for Container.member to get a member's source):`);
|
|
323
|
+
out.push(...memberOutline(ctx, sym, Math.max(10, Math.min(60, maxLines))));
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
// The doc comment is shown, but it must not eat the budget meant for code.
|
|
327
|
+
if (hasDoc) {
|
|
328
|
+
const docCount = sym.start_line - sym.doc_line;
|
|
329
|
+
const docEnd = docCount > MAX_DOC_LINES ? sym.doc_line + MAX_DOC_LINES - 5 : sym.start_line - 1;
|
|
330
|
+
const text = ctx.lines.excerpt(sym.path, sym.doc_line, docEnd);
|
|
331
|
+
if (text !== null)
|
|
332
|
+
out.push(text);
|
|
333
|
+
if (docEnd < sym.start_line - 1)
|
|
334
|
+
out.push(` ... (${sym.start_line - 1 - docEnd} more doc lines)`);
|
|
335
|
+
}
|
|
336
|
+
const end = Math.min(sym.end_line, sym.start_line + maxLines - 1);
|
|
337
|
+
const text = ctx.lines.excerpt(sym.path, sym.start_line, end);
|
|
338
|
+
if (text === null) {
|
|
339
|
+
out.push(sym.signature, '(source file is not readable)');
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
out.push(text);
|
|
343
|
+
if (end < sym.end_line) {
|
|
344
|
+
out.push(`... ${sym.end_line - end} more lines (${sym.path}:${end + 1}-${sym.end_line}); raise maxLines to see them`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (isContainer || sym.kind === 'type')
|
|
349
|
+
out.push(...outsideMethods(ctx, sym, 40));
|
|
350
|
+
if (overloads.length) {
|
|
351
|
+
out.push(`overloads / other declarations here (address one as ${sym.path}:<line>):`);
|
|
352
|
+
for (const o of overloads.slice(0, 6))
|
|
353
|
+
out.push(` ${range(o).padEnd(9)} ${o.signature}`);
|
|
354
|
+
if (overloads.length > 6)
|
|
355
|
+
out.push(` ... ${overloads.length - 6} more`);
|
|
356
|
+
}
|
|
357
|
+
// Type relationships.
|
|
358
|
+
if (isContainer || sym.kind === 'type') {
|
|
359
|
+
const supers = directSupertypes(ctx.db, ctx.resolver, sym);
|
|
360
|
+
if (supers.length) {
|
|
361
|
+
out.push('inherits: ' + supers.map(h => `${h.kind} ${h.name}${h.sym ? ` (${h.sym.path}:${h.sym.start_line})` : ''}`).join(', '));
|
|
362
|
+
}
|
|
363
|
+
const satisfied = structuralInterfaces(ctx.db, sym, 12);
|
|
364
|
+
if (satisfied.length)
|
|
365
|
+
out.push('satisfies (by method set): ' + satisfied.map(s => s.qname).join(', '));
|
|
366
|
+
subtypeLines(ctx, sym, 8, out);
|
|
367
|
+
}
|
|
368
|
+
else if (sym.kind === 'method') {
|
|
369
|
+
// Same method on related types: the interface declaration, overrides, implementations.
|
|
370
|
+
const family = [...ctx.resolver.familyOf(sym)]
|
|
371
|
+
.filter(id => id !== sym.id)
|
|
372
|
+
.map(id => ctx.db.symbolById(id))
|
|
373
|
+
.filter((s) => !!s && !(s.file_id === sym.file_id && s.qname === sym.qname));
|
|
374
|
+
if (family.length) {
|
|
375
|
+
out.push(`same method on related types (${family.length}; calls through a supertype or interface count as callers):`);
|
|
376
|
+
for (const f of family.slice(0, 6))
|
|
377
|
+
out.push(` ${f.qname} ${loc(f)}`);
|
|
378
|
+
if (family.length > 6)
|
|
379
|
+
out.push(` ... ${family.length - 6} more (clast_hierarchy ${sym.qname})`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (args.includeCalls === false)
|
|
383
|
+
return out;
|
|
384
|
+
const callable = CALLABLE_KINDS.has(sym.kind);
|
|
385
|
+
if (callable || isContainer || MINOR_KINDS.has(sym.kind)) {
|
|
386
|
+
// A summary: bounded work even for names with tens of thousands of call sites.
|
|
387
|
+
const callerStats = { examined: 0, total: 0 };
|
|
388
|
+
const callers = callersOf(ctx.db, ctx.resolver, sym, { quick: true, overloads }, callerStats);
|
|
389
|
+
callerLines(callers, out);
|
|
390
|
+
if (callerStats.total > callerStats.examined) {
|
|
391
|
+
out.push(` ("${sym.name}" has ${callerStats.total} call sites; this summary examined ${callerStats.examined}. ` +
|
|
392
|
+
`clast_call_graph with within=<path> covers an area completely.)`);
|
|
393
|
+
}
|
|
394
|
+
const resolved = callers.filter(c => c.certain && !isTestPath(c.path)).length;
|
|
395
|
+
if (callable && callers.length === 0)
|
|
396
|
+
out.push('callers: none found');
|
|
397
|
+
if ((callable || MINOR_KINDS.has(sym.kind)) && resolved < 3) {
|
|
398
|
+
const refs = valueReferences(ctx, sym, callers, 6);
|
|
399
|
+
if (refs.length) {
|
|
400
|
+
out.push('referenced without a call (passed as a value, exported, used as a type):');
|
|
401
|
+
out.push(...refs);
|
|
402
|
+
}
|
|
403
|
+
else if (!callable && callers.length === 0) {
|
|
404
|
+
const n = ctx.db.refFileCount(sym.name);
|
|
405
|
+
if (n > 1)
|
|
406
|
+
out.push(`referenced in ${plural(n, 'file')} (clast_references ${sym.name})`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (callable) {
|
|
411
|
+
const callees = calleesOf(ctx.db, ctx.resolver, sym);
|
|
412
|
+
const internal = callees.filter(c => c.target);
|
|
413
|
+
const external = callees.filter(c => !c.target);
|
|
414
|
+
if (internal.length) {
|
|
415
|
+
out.push(`calls (${internal.length}):`);
|
|
416
|
+
for (const c of internal.slice(0, 20)) {
|
|
417
|
+
out.push(` ${c.target.qname} ${loc(c.target)}${c.certain ? '' : ' ?'}`);
|
|
418
|
+
}
|
|
419
|
+
if (internal.length > 20)
|
|
420
|
+
out.push(` ... ${internal.length - 20} more (clast_call_graph direction=callees)`);
|
|
421
|
+
}
|
|
422
|
+
if (external.length) {
|
|
423
|
+
const names = [...new Set(external.map(c => c.name))];
|
|
424
|
+
out.push(`external calls: ${names.slice(0, 24).join(', ')}${names.length > 24 ? ', ...' : ''}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
429
|
+
export function notFound(ctx, name, path) {
|
|
430
|
+
const out = [];
|
|
431
|
+
const bare = parseSymbolRef(name).name || name;
|
|
432
|
+
const { hits } = searchSymbols(ctx.db, bare, { limit: 6, path });
|
|
433
|
+
out.push(`"${name}": no symbol with this name in the index.`);
|
|
434
|
+
if (hits.length) {
|
|
435
|
+
out.push('closest matches:');
|
|
436
|
+
for (const h of hits)
|
|
437
|
+
out.push(` ${symLine(h.sym)}`);
|
|
438
|
+
}
|
|
439
|
+
const uses = ctx.db.refFileCount(bare);
|
|
440
|
+
out.push(uses > 0
|
|
441
|
+
? `"${bare}" is used in ${plural(uses, 'file')} but not defined here (external, generated or defined dynamically): ` +
|
|
442
|
+
`clast_references ${bare} lists the usages, clast_grep finds dynamic definitions.`
|
|
443
|
+
: `It may be defined dynamically (metaprogramming, string keys) or in a non-indexed file: try clast_grep "${bare}".`);
|
|
444
|
+
return out;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* `Widget.layout` was asked for but the answer is `ChatWidget.layout` or an inherited
|
|
448
|
+
* `Composite.layout`: say so, instead of letting the reader assume it is what they named.
|
|
449
|
+
*/
|
|
450
|
+
export function containerNote(asked, sym) {
|
|
451
|
+
const ref = parseSymbolRef(asked);
|
|
452
|
+
// `Stream` asked, only `stream` exists: say that the match is not exact.
|
|
453
|
+
if (ref.name && ref.name !== sym.name && ref.name.toLowerCase() === sym.name.toLowerCase()) {
|
|
454
|
+
return [`note: nothing is named exactly "${ref.name}"; this is ${sym.kind} "${sym.name}" (case-insensitive match).`];
|
|
455
|
+
}
|
|
456
|
+
const want = ref.container[ref.container.length - 1];
|
|
457
|
+
if (!want)
|
|
458
|
+
return [];
|
|
459
|
+
const parts = sym.qname.split('.');
|
|
460
|
+
const have = parts.length >= 2 ? parts[parts.length - 2] : '';
|
|
461
|
+
if (have.toLowerCase() === want.toLowerCase())
|
|
462
|
+
return [];
|
|
463
|
+
// The container may be a package / module / file rather than a type.
|
|
464
|
+
const inPath = sym.path.toLowerCase().split(/[\/.]/).includes(want.toLowerCase());
|
|
465
|
+
if (inPath || parts.slice(0, -1).some(p => p.toLowerCase() === want.toLowerCase()))
|
|
466
|
+
return [];
|
|
467
|
+
return [
|
|
468
|
+
`note: "${want}" does not define "${sym.name}" itself; this is ${sym.qname} ` +
|
|
469
|
+
`(inherited by ${want}, or the closest match - check that it is the one you mean).`,
|
|
470
|
+
];
|
|
471
|
+
}
|
|
472
|
+
export async function getContext(ctx, args) {
|
|
473
|
+
const note = await ctx.ready();
|
|
474
|
+
const names = toList(args.name);
|
|
475
|
+
if (names.length === 0)
|
|
476
|
+
return 'error: name is required';
|
|
477
|
+
const out = [];
|
|
478
|
+
for (const name of names) {
|
|
479
|
+
let matches = findSymbols(ctx.db, name, { path: args.path });
|
|
480
|
+
if (matches.length) {
|
|
481
|
+
// Line numbers and source must reflect the file as it is on disk now.
|
|
482
|
+
await ctx.fresh(new Set(matches.slice(0, 3).map(m => m.path)));
|
|
483
|
+
matches = findSymbols(ctx.db, name, { path: args.path });
|
|
484
|
+
}
|
|
485
|
+
if (matches.length === 0) {
|
|
486
|
+
out.push(...notFound(ctx, name, args.path), '');
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const chosen = matches[0];
|
|
490
|
+
// (A prototype above its definition, or a field next to its accessor, is not an overload.)
|
|
491
|
+
const overloads = matches.filter(m => m.id !== chosen.id && m.file_id === chosen.file_id && m.qname === chosen.qname && m.kind === chosen.kind);
|
|
492
|
+
const { others } = otherDefinitions(chosen, matches, ctx.resolver.familyOf(chosen));
|
|
493
|
+
const ref = parseSymbolRef(name);
|
|
494
|
+
const unqualified = ref.container.length === 0 && !ref.path && !args.path && ref.line === undefined;
|
|
495
|
+
if (unqualified && others.length >= 4) {
|
|
496
|
+
// Many unrelated definitions share this name: listing them is worth more than guessing one.
|
|
497
|
+
out.push(`"${name}" has ${others.length + 1} definitions. Pick one with Container.name or path:name:`);
|
|
498
|
+
for (const m of [chosen, ...others].slice(0, 14)) {
|
|
499
|
+
out.push(` ${symLine(m)}${m.signature ? ' ' + firstLine(m.signature, 70) : ''}`);
|
|
500
|
+
}
|
|
501
|
+
if (others.length + 1 > 14)
|
|
502
|
+
out.push(` ... ${others.length + 1 - 14} more (clast_search ${name} with kind/path filters)`);
|
|
503
|
+
const size = chosen.end_line - chosen.start_line + 1;
|
|
504
|
+
if (size <= 40) {
|
|
505
|
+
out.push('most prominent definition:');
|
|
506
|
+
out.push(...(await describe(ctx, chosen, { ...args, includeCalls: false }, overloads)));
|
|
507
|
+
}
|
|
508
|
+
out.push('');
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
out.push(...containerNote(name, chosen));
|
|
512
|
+
out.push(...(await describe(ctx, chosen, args, overloads)));
|
|
513
|
+
if (others.length) {
|
|
514
|
+
out.push(`other definitions named "${chosen.name}" (${others.length}; select with Container.name or path:name):`);
|
|
515
|
+
for (const m of others.slice(0, 6))
|
|
516
|
+
out.push(` ${symLine(m)}`);
|
|
517
|
+
if (others.length > 6)
|
|
518
|
+
out.push(` ... ${others.length - 6} more`);
|
|
519
|
+
}
|
|
520
|
+
out.push('');
|
|
521
|
+
}
|
|
522
|
+
return note + out.join('\n').trimEnd();
|
|
523
|
+
}
|
|
524
|
+
function renderGraph(node, indent, out, direction) {
|
|
525
|
+
for (const child of node.children) {
|
|
526
|
+
const pad = ' '.repeat(indent);
|
|
527
|
+
const name = child.sym ? child.sym.qname : child.label ?? '<file level>';
|
|
528
|
+
const where = direction === 'callers' ? `${child.path}:${child.line}` : child.sym ? loc(child.sym) : '';
|
|
529
|
+
const tags = (child.sites > 1 ? ` (x${child.sites})` : '') +
|
|
530
|
+
(child.weak ? ' ? (receiver type unknown)' : child.certain ? '' : ' ?') +
|
|
531
|
+
(child.repeated ? ' (shown above)' : '') +
|
|
532
|
+
(child.hub ? ` (${child.hub} ${direction}, not expanded: query it directly)` : '');
|
|
533
|
+
out.push(`${pad}${name} ${where}${tags}`);
|
|
534
|
+
renderGraph(child, indent + 1, out, direction);
|
|
535
|
+
}
|
|
536
|
+
if (node.omitted)
|
|
537
|
+
out.push(`${' '.repeat(indent)}... ${node.omitted} more (raise limit, or narrow with within)`);
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* When a bare name has several unrelated definitions, say so before answering
|
|
541
|
+
* (and stop when there are too many to guess). Returns lines to print and
|
|
542
|
+
* whether the caller should continue with the first match.
|
|
543
|
+
*/
|
|
544
|
+
export function disambiguate(ctx, name, path, matches) {
|
|
545
|
+
const chosen = matches[0];
|
|
546
|
+
const all = otherDefinitions(chosen, matches, ctx.resolver.familyOf(chosen)).others;
|
|
547
|
+
// Verbatim copies of the same definition (vendored forks, generated duplicates) are not real alternatives.
|
|
548
|
+
const span = chosen.end_line - chosen.start_line;
|
|
549
|
+
const copies = all.filter(o => o.qname === chosen.qname && o.kind === chosen.kind && o.signature === chosen.signature && o.end_line - o.start_line === span);
|
|
550
|
+
const others = all.filter(o => !copies.includes(o));
|
|
551
|
+
if (others.length === 0) {
|
|
552
|
+
return {
|
|
553
|
+
lines: copies.length
|
|
554
|
+
? [`(identical cop${copies.length === 1 ? 'y' : 'ies'} elsewhere: ${copies.slice(0, 2).map(c => c.path + ':' + c.start_line).join(', ')}${copies.length > 2 ? ', ...' : ''})`]
|
|
555
|
+
: [],
|
|
556
|
+
stop: false,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
const ref = parseSymbolRef(name);
|
|
560
|
+
const unqualified = ref.container.length === 0 && !ref.path && !path && ref.line === undefined;
|
|
561
|
+
// One definition that the rest of the project overwhelmingly imports is "the" one
|
|
562
|
+
// (lifecycle.ts:Disposable vs a few same-named helpers in extensions): do not make the caller choose.
|
|
563
|
+
const importers = (s) => ctx.db.get(`SELECT COUNT(DISTINCT file_id) AS n FROM imports WHERE resolved_id = ?`, s.file_id)?.n ?? 0;
|
|
564
|
+
const mine = unqualified && others.length >= 4 ? importers(chosen) : 0;
|
|
565
|
+
const dominant = mine >= 20 && others.slice(0, 12).every(o => importers(o) * 10 <= mine);
|
|
566
|
+
if (unqualified && others.length >= 4 && !dominant) {
|
|
567
|
+
const lines = [`"${name}" has ${others.length + 1} definitions. Pick one with Container.name or path:name:`];
|
|
568
|
+
for (const m of [chosen, ...others].slice(0, 14)) {
|
|
569
|
+
lines.push(` ${symLine(m)}${m.signature ? ' ' + firstLine(m.signature, 70) : ''}`);
|
|
570
|
+
}
|
|
571
|
+
if (others.length + 1 > 14)
|
|
572
|
+
lines.push(` ... ${others.length + 1 - 14} more (clast_search ${name} with kind/path filters)`);
|
|
573
|
+
return { lines, stop: true };
|
|
574
|
+
}
|
|
575
|
+
const lines = [`note: "${chosen.name}" has ${others.length} other definition(s); using the first. Others (select with Container.name or path:name):`];
|
|
576
|
+
for (const m of others.slice(0, 4))
|
|
577
|
+
lines.push(` ${symLine(m)}`);
|
|
578
|
+
return { lines, stop: false };
|
|
579
|
+
}
|
|
580
|
+
export async function callGraph(ctx, args) {
|
|
581
|
+
const note = await ctx.ready();
|
|
582
|
+
if (!args.name || !args.name.trim())
|
|
583
|
+
return 'error: name is required';
|
|
584
|
+
const matches = findSymbols(ctx.db, args.name, { path: args.path });
|
|
585
|
+
const direction = args.direction ?? 'both';
|
|
586
|
+
const depth = Math.max(1, Math.min(args.depth ?? 1, 6));
|
|
587
|
+
const budget = Math.max(1, Math.min(args.limit ?? 50, 2000));
|
|
588
|
+
const within = args.within;
|
|
589
|
+
const pathFilter = within ? (p) => pathMatches(p, within) : undefined;
|
|
590
|
+
// Tests: listed in full only on request (or when the scope itself is a test / example tree);
|
|
591
|
+
// by default they are summarised per file.
|
|
592
|
+
const listTests = args.includeTests === true || (within !== undefined && isTestPath(within.replace(/\/?$/, '/')));
|
|
593
|
+
const dropTests = args.includeTests === false;
|
|
594
|
+
if (matches.length === 0) {
|
|
595
|
+
// Not defined here (library function, dynamic method): still answer "who calls it".
|
|
596
|
+
const bare = parseSymbolRef(args.name).name || args.name;
|
|
597
|
+
const sites = groupCallers(externalCallSites(ctx.db, bare)).filter(g => (!dropTests || !isTestPath(g.path)) && (!pathFilter || pathFilter(g.path)));
|
|
598
|
+
// "Foo.bar" where bar exists but not on Foo (an extension method, a typo in the qualifier): not external.
|
|
599
|
+
const definedElsewhere = ctx.db.symbolsByName(bare).length > 0;
|
|
600
|
+
if (sites.length === 0 || direction === 'callees' || definedElsewhere) {
|
|
601
|
+
return note + notFound(ctx, args.name, args.path).join('\n');
|
|
602
|
+
}
|
|
603
|
+
const out = [
|
|
604
|
+
`"${bare}" is not defined in this project (external or dynamic). Call sites by name (${plural(sites.length, 'caller')}):`,
|
|
605
|
+
];
|
|
606
|
+
for (const g of sites.slice(0, budget)) {
|
|
607
|
+
out.push(` ${g.caller ? g.caller.qname : '<file level>'} ${g.path}:${g.line}${g.sites > 1 ? ` (x${g.sites})` : ''}`);
|
|
608
|
+
}
|
|
609
|
+
if (sites.length > budget)
|
|
610
|
+
out.push(` ... ${sites.length - budget} more (raise limit or narrow with within)`);
|
|
611
|
+
return note + out.join('\n');
|
|
612
|
+
}
|
|
613
|
+
const amb = disambiguate(ctx, args.name, args.path, matches);
|
|
614
|
+
if (amb.stop)
|
|
615
|
+
return note + amb.lines.join('\n');
|
|
616
|
+
const sym = matches[0];
|
|
617
|
+
const out = [symLine(sym)];
|
|
618
|
+
if (sym.signature)
|
|
619
|
+
out.push(` ${firstLine(sym.signature, 160)}`);
|
|
620
|
+
out.push(...amb.lines);
|
|
621
|
+
out.push(...containerNote(args.name, sym));
|
|
622
|
+
// Overloads are told apart by argument types, which call sites do not record: say that they are merged.
|
|
623
|
+
const overloads = matches.filter(m => m.id !== sym.id && m.file_id === sym.file_id && m.qname === sym.qname && m.kind === sym.kind);
|
|
624
|
+
// A bare name means any overload; `path:line` means that one (matched by argument count).
|
|
625
|
+
const oneOverload = parseSymbolRef(args.name).line !== undefined;
|
|
626
|
+
if (overloads.length) {
|
|
627
|
+
const lines = `${overloads.slice(0, 4).map(o => o.start_line).join(', ')}${overloads.length > 4 ? ', ...' : ''}`;
|
|
628
|
+
out.push(oneOverload
|
|
629
|
+
? `(${plural(overloads.length, 'other overload')} at line ${lines}; only call sites whose argument count fits this one are listed)`
|
|
630
|
+
: `(${plural(overloads.length, 'other overload')} at line ${lines}; callers of all overloads are listed - ` +
|
|
631
|
+
`for one overload only, address it by line: ${sym.path}:${overloads[0].start_line})`);
|
|
632
|
+
}
|
|
633
|
+
const stats = { examined: 0, total: 0 };
|
|
634
|
+
// A plain directory / file fragment can be pre-filtered by the database.
|
|
635
|
+
const pathContains = within && !/[*?]/.test(within) ? within.replace(/\\/g, '/').replace(/^\.\//, '') : undefined;
|
|
636
|
+
const opts = {
|
|
637
|
+
depth, budget: 100000, includeTests: !dropTests, path: pathFilter, pathContains, stats,
|
|
638
|
+
overloads: oneOverload ? undefined : overloads,
|
|
639
|
+
};
|
|
640
|
+
const accept = (p) => (!dropTests || !isTestPath(p)) && (!pathFilter || pathFilter(p));
|
|
641
|
+
if (direction !== 'callees') {
|
|
642
|
+
const graph = buildGraph(ctx.db, ctx.resolver, sym, 'callers', opts);
|
|
643
|
+
const all = graph.children;
|
|
644
|
+
const tests = all.filter(c => isTestSite(c.path, c.sym?.qname));
|
|
645
|
+
const prod = all.filter(c => !isTestSite(c.path, c.sym?.qname));
|
|
646
|
+
const certain = prod.filter(c => c.certain);
|
|
647
|
+
const possible = prod.filter(c => !c.certain && !c.weak);
|
|
648
|
+
const unknown = prod.filter(c => c.weak);
|
|
649
|
+
const parts = [];
|
|
650
|
+
if (certain.length)
|
|
651
|
+
parts.push(`${certain.length} resolved`);
|
|
652
|
+
if (possible.length)
|
|
653
|
+
parts.push(`${possible.length} name-only "?"`);
|
|
654
|
+
if (unknown.length)
|
|
655
|
+
parts.push(`${unknown.length} on receivers of unknown type`);
|
|
656
|
+
if (tests.length)
|
|
657
|
+
parts.push(`${tests.length} in tests/examples`);
|
|
658
|
+
out.push(`callers (${parts.length ? parts.join(', ') : '0'}${depth > 1 ? `; depth ${depth}` : ''}; caller call-site):`);
|
|
659
|
+
let remaining = budget;
|
|
660
|
+
const emit = (nodes, cap, more) => {
|
|
661
|
+
const take = Math.min(nodes.length, cap, remaining);
|
|
662
|
+
renderGraph({ ...graph, children: nodes.slice(0, take), omitted: 0 }, 1, out, 'callers');
|
|
663
|
+
remaining -= take;
|
|
664
|
+
if (nodes.length > take)
|
|
665
|
+
out.push(` ... ${nodes.length - take} more ${more}`);
|
|
666
|
+
};
|
|
667
|
+
if (all.length === 0)
|
|
668
|
+
out.push(' none found');
|
|
669
|
+
emit(certain, budget, '(raise limit, or narrow with within)');
|
|
670
|
+
// Unconfirmed edges are summarised unless asked for, or unless they are all there is.
|
|
671
|
+
const showAll = args.uncertain === true || certain.length === 0;
|
|
672
|
+
if (possible.length)
|
|
673
|
+
emit(possible, showAll ? budget : 5, 'name-only matches (uncertain=true lists them)');
|
|
674
|
+
if (unknown.length)
|
|
675
|
+
emit(unknown, showAll ? budget : 3, 'call sites on receivers of unknown type (uncertain=true lists them)');
|
|
676
|
+
if (certain.length === 0) {
|
|
677
|
+
// Nothing confirmed: the symbol may be used as a value (callback, property read, export) instead.
|
|
678
|
+
const refs = valueReferences(ctx, sym, [], 6, accept);
|
|
679
|
+
if (refs.length) {
|
|
680
|
+
out.push('referenced without a call (passed as a value, read as a property, exported):');
|
|
681
|
+
out.push(...refs);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (stats.total > stats.examined) {
|
|
685
|
+
out.push(` note: "${sym.name}" is called in ${stats.total} places; ${stats.examined} were examined (files that import the definition first). ` +
|
|
686
|
+
`Narrow with within to cover a specific area completely.`);
|
|
687
|
+
}
|
|
688
|
+
if (tests.length) {
|
|
689
|
+
if (listTests) {
|
|
690
|
+
out.push('in tests/examples:');
|
|
691
|
+
emit(tests, budget, '(raise limit)');
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
const files = [...new Set(tests.map(t => t.path))];
|
|
695
|
+
out.push(` tests/examples (${tests.length} callers in ${plural(files.length, 'file')}; includeTests=true lists them): ` +
|
|
696
|
+
`${files.slice(0, 5).join(', ')}${files.length > 5 ? ', ...' : ''}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (direction !== 'callers') {
|
|
701
|
+
const graph = buildGraph(ctx.db, ctx.resolver, sym, 'callees', { ...opts, budget });
|
|
702
|
+
const total = graph.children.length + (graph.omitted ?? 0);
|
|
703
|
+
out.push(`callees (${total}${depth > 1 ? `, depth ${depth}` : ''}; callee definition):`);
|
|
704
|
+
if (graph.children.length === 0)
|
|
705
|
+
out.push(' none resolved inside this project');
|
|
706
|
+
renderGraph(graph, 1, out, 'callees');
|
|
707
|
+
const rest = calleesOf(ctx.db, ctx.resolver, sym).filter(c => !c.target);
|
|
708
|
+
const unresolved = [...new Set(rest.filter(c => c.alternatives > 0).map(c => `${c.name} (${c.alternatives} defs)`))];
|
|
709
|
+
const external = [...new Set(rest.filter(c => c.alternatives === 0).map(c => c.name))];
|
|
710
|
+
if (unresolved.length) {
|
|
711
|
+
out.push(` unresolved (receiver type unknown; project defines these names): ${unresolved.slice(0, 16).join(', ')}${unresolved.length > 16 ? ', ...' : ''}`);
|
|
712
|
+
}
|
|
713
|
+
if (external.length)
|
|
714
|
+
out.push(` external: ${external.slice(0, 30).join(', ')}${external.length > 30 ? ', ...' : ''}`);
|
|
715
|
+
}
|
|
716
|
+
if (out.some(l => / \?( |$)/.test(l))) {
|
|
717
|
+
out.push('("?" = matched by name only; the receiver type is unknown and another definition could be the real target)');
|
|
718
|
+
}
|
|
719
|
+
return note + out.join('\n');
|
|
720
|
+
}
|
|
721
|
+
export async function hierarchy(ctx, args) {
|
|
722
|
+
const note = await ctx.ready();
|
|
723
|
+
if (!args.name || !args.name.trim())
|
|
724
|
+
return 'error: name is required';
|
|
725
|
+
let matches = findSymbols(ctx.db, args.name, { path: args.path });
|
|
726
|
+
if (matches.length === 0)
|
|
727
|
+
return note + notFound(ctx, args.name, args.path).join('\n');
|
|
728
|
+
// A bare type name means the type, not its constructors or same-named members.
|
|
729
|
+
const types = matches.filter(m => CONTAINER_KINDS.has(m.kind) || m.kind === 'type');
|
|
730
|
+
if (types.length && parseSymbolRef(args.name).container.length === 0)
|
|
731
|
+
matches = types;
|
|
732
|
+
// A C# static class has no place in a hierarchy: prefer a same-named class that can.
|
|
733
|
+
// (Likewise a sealed class cannot have subtypes: `ActionResult<T>` vs the base class `ActionResult`.)
|
|
734
|
+
const isStatic = (s) => s.lang === 'csharp' && /\b(static|sealed)\s+(partial\s+)?class\b/.test(s.signature);
|
|
735
|
+
if (matches.length > 1 && isStatic(matches[0])) {
|
|
736
|
+
const other = matches.findIndex(m => !isStatic(m));
|
|
737
|
+
if (other > 0)
|
|
738
|
+
matches = [matches[other], ...matches.filter((_, i) => i !== other)];
|
|
739
|
+
}
|
|
740
|
+
const amb = disambiguate(ctx, args.name, args.path, matches);
|
|
741
|
+
if (amb.stop)
|
|
742
|
+
return note + amb.lines.join('\n');
|
|
743
|
+
const sym = matches[0];
|
|
744
|
+
const direction = args.direction ?? 'both';
|
|
745
|
+
const depth = Math.max(1, Math.min(args.depth ?? 3, 8));
|
|
746
|
+
const limit = Math.max(1, Math.min(args.limit ?? 80, 1000));
|
|
747
|
+
const out = [symLine(sym)];
|
|
748
|
+
if (sym.signature)
|
|
749
|
+
out.push(` ${firstLine(sym.signature, 160)}`);
|
|
750
|
+
out.push(...amb.lines);
|
|
751
|
+
out.push(...containerNote(args.name, sym));
|
|
752
|
+
// A method: show the same method across the type hierarchy.
|
|
753
|
+
if (!CONTAINER_KINDS.has(sym.kind) && sym.kind !== 'type') {
|
|
754
|
+
const family = [...ctx.resolver.familyOf(sym)]
|
|
755
|
+
.filter(id => id !== sym.id)
|
|
756
|
+
.map(id => ctx.db.symbolById(id))
|
|
757
|
+
.filter((s) => !!s);
|
|
758
|
+
const total = family.length;
|
|
759
|
+
if (args.within)
|
|
760
|
+
family.splice(0, family.length, ...family.filter(f => pathMatches(f.path, args.within)));
|
|
761
|
+
if (family.length === 0) {
|
|
762
|
+
out.push(total
|
|
763
|
+
? `no overrides under "${args.within}" (${total} elsewhere).`
|
|
764
|
+
: 'no overrides, implementations or declarations of this member on related types.');
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
out.push(`declarations / overrides / implementations on related types (${family.length}${args.within ? ` under ${args.within}, ${total} in total` : ''}):`);
|
|
768
|
+
family.sort((a, b) => Number(isTestPath(a.path)) - Number(isTestPath(b.path)) || a.path.localeCompare(b.path));
|
|
769
|
+
for (const f of family.slice(0, limit))
|
|
770
|
+
out.push(` ${f.kind} ${f.qname} ${loc(f)}`);
|
|
771
|
+
if (family.length > limit)
|
|
772
|
+
out.push(` ... ${family.length - limit} more (raise limit)`);
|
|
773
|
+
}
|
|
774
|
+
return note + out.join('\n');
|
|
775
|
+
}
|
|
776
|
+
if (direction !== 'subtypes') {
|
|
777
|
+
out.push('supertypes:');
|
|
778
|
+
let lines = 0;
|
|
779
|
+
const seen = new Set([sym.id]);
|
|
780
|
+
const walkUp = (s, indent) => {
|
|
781
|
+
if (indent > depth || lines >= limit)
|
|
782
|
+
return;
|
|
783
|
+
for (const sup of directSupertypes(ctx.db, ctx.resolver, s)) {
|
|
784
|
+
if (lines >= limit)
|
|
785
|
+
return;
|
|
786
|
+
lines++;
|
|
787
|
+
out.push(`${' '.repeat(indent)}${sup.kind} ${sup.sym ? sup.sym.qname + ' ' + loc(sup.sym) : sup.name + ' (not in this project)'}`);
|
|
788
|
+
if (sup.sym && !seen.has(sup.sym.id)) {
|
|
789
|
+
seen.add(sup.sym.id);
|
|
790
|
+
walkUp(sup.sym, indent + 1);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
walkUp(sym, 1);
|
|
795
|
+
if (lines === 0)
|
|
796
|
+
out.push(' none declared');
|
|
797
|
+
const satisfied = structuralInterfaces(ctx.db, sym, 60);
|
|
798
|
+
if (satisfied.length) {
|
|
799
|
+
out.push(`satisfies by method set (${satisfied.length}):`);
|
|
800
|
+
for (const s of satisfied.slice(0, limit))
|
|
801
|
+
out.push(` ${s.qname} ${loc(s)}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (direction !== 'supertypes') {
|
|
805
|
+
let lines = 0;
|
|
806
|
+
let total = 0;
|
|
807
|
+
const seen = new Set([sym.id]);
|
|
808
|
+
const body = [];
|
|
809
|
+
const walkDown = (s, indent) => {
|
|
810
|
+
if (indent > depth)
|
|
811
|
+
return;
|
|
812
|
+
const subs = directSubtypes(ctx.db, ctx.resolver, s);
|
|
813
|
+
for (const sub of subs) {
|
|
814
|
+
const shown = !args.within || pathMatches(sub.sym.path, args.within);
|
|
815
|
+
if (shown)
|
|
816
|
+
total++;
|
|
817
|
+
if (shown && lines < limit) {
|
|
818
|
+
lines++;
|
|
819
|
+
body.push(`${' '.repeat(indent)}${sub.sym.kind} ${sub.sym.qname} ${sub.sym.path}:${sub.sym.start_line}${sub.certain ? '' : ' ?'}`);
|
|
820
|
+
}
|
|
821
|
+
if (!seen.has(sub.sym.id) && total < 5000) {
|
|
822
|
+
seen.add(sub.sym.id);
|
|
823
|
+
walkDown(sub.sym, indent + 1);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
walkDown(sym, 1);
|
|
828
|
+
out.push(`subtypes (${total}${depth > 1 ? ', transitive' : ''}):`);
|
|
829
|
+
out.push(...(body.length ? body : [' none found']));
|
|
830
|
+
if (total > lines)
|
|
831
|
+
out.push(` ... ${total - lines} more (raise limit)`);
|
|
832
|
+
const structural = structuralImplementers(ctx.db, sym, 500);
|
|
833
|
+
if (structural.length) {
|
|
834
|
+
out.push(`implemented by, matched by method set (${structural.length}):`);
|
|
835
|
+
for (const s of structural.slice(0, limit))
|
|
836
|
+
out.push(` ${s.kind} ${s.qname} ${s.path}:${s.start_line}`);
|
|
837
|
+
if (structural.length > limit)
|
|
838
|
+
out.push(` ... ${structural.length - limit} more (raise limit)`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return note + out.join('\n');
|
|
842
|
+
}
|
|
843
|
+
//# sourceMappingURL=search.js.map
|