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,447 @@
|
|
|
1
|
+
import type { ClastDb } from '../db/index.js';
|
|
2
|
+
import { SYMBOL_COLUMNS, SYMBOL_FROM } from '../db/index.js';
|
|
3
|
+
import { CONTAINER_KINDS, type SymbolRow } from '../types.js';
|
|
4
|
+
import { isTestPath } from './format.js';
|
|
5
|
+
import { genericArity } from '../parser/spec.js';
|
|
6
|
+
import { langFamily, type Resolver } from './resolve.js';
|
|
7
|
+
|
|
8
|
+
/** Type parameters a type is declared with: `class ActionResult<TValue>` -> 1. */
|
|
9
|
+
function declaredArity(sym: SymbolRow): number {
|
|
10
|
+
const at = sym.signature.search(new RegExp('\\b' + sym.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*<'));
|
|
11
|
+
return at === -1 ? 0 : genericArity(sym.signature.slice(at));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const isPartial = (s: SymbolRow): boolean => /\bpartial\b/.test(s.signature);
|
|
15
|
+
/** C# static classes cannot be inherited from. */
|
|
16
|
+
const isStaticClass = (s: SymbolRow): boolean => s.lang === 'csharp' && /\bstatic\s+(partial\s+)?class\b/.test(s.signature);
|
|
17
|
+
|
|
18
|
+
export interface TypeEdge {
|
|
19
|
+
sym: SymbolRow;
|
|
20
|
+
/** 'extends' | 'implements' | 'structural' */
|
|
21
|
+
kind: string;
|
|
22
|
+
/** False when the name matches but another definition of that name could be meant. */
|
|
23
|
+
certain: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SuperEdge {
|
|
27
|
+
name: string;
|
|
28
|
+
kind: string;
|
|
29
|
+
/** Resolved definition; null for types outside the project. */
|
|
30
|
+
sym: SymbolRow | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const RUST_RELATIVE = new Set(['crate', 'self', 'super', 'Self']);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Can `qualifier.Name` / `qualifier::Name` written in Go or Rust mean the type `cand`?
|
|
37
|
+
* The qualifier is a package or module, which shows in the path of the defining file
|
|
38
|
+
* (directory or file name). Other languages: always true. `strict` rejects relative paths too.
|
|
39
|
+
*/
|
|
40
|
+
function qualifierFits(qualifier: string, cand: SymbolRow, lang: string, strict = false): boolean {
|
|
41
|
+
if (lang !== 'go' && lang !== 'rust') return true;
|
|
42
|
+
const q = qualifier.split(/::|\./).filter(Boolean);
|
|
43
|
+
const last = q[q.length - 1];
|
|
44
|
+
if (!last) return true;
|
|
45
|
+
if (lang === 'rust' && RUST_RELATIVE.has(last)) return !strict;
|
|
46
|
+
const segs = cand.path.toLowerCase().split('/');
|
|
47
|
+
const want = last.toLowerCase().replace(/_/g, '-');
|
|
48
|
+
for (let i = 0; i < segs.length; i++) {
|
|
49
|
+
const seg = (i === segs.length - 1 ? segs[i]!.replace(/\.[a-z]+$/, '') : segs[i]!).replace(/_/g, '-');
|
|
50
|
+
if (seg === want) return true;
|
|
51
|
+
}
|
|
52
|
+
// An enclosing module / type of the same name inside the file.
|
|
53
|
+
return cand.qname.split('.').slice(0, -1).some(p => p.toLowerCase() === last.toLowerCase());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Lexical scope lookup of a type name written inside `from`: `Enum` inside
|
|
58
|
+
* `ActiveRecord.Base` is `ActiveRecord.Base.Enum`, else `ActiveRecord.Enum`,
|
|
59
|
+
* else top-level `Enum`. Returns null when no enclosing scope defines it.
|
|
60
|
+
*/
|
|
61
|
+
function lexicalType(db: ClastDb, from: SymbolRow, qualifier: string | null, name: string): SymbolRow | null {
|
|
62
|
+
const cands = db
|
|
63
|
+
.symbolsByName(name)
|
|
64
|
+
.filter(s => (CONTAINER_KINDS.has(s.kind) || s.kind === 'type') && s.id !== from.id && s.lang === from.lang && !isStaticClass(s));
|
|
65
|
+
if (cands.length === 0) return null;
|
|
66
|
+
const parts = from.qname.split('.');
|
|
67
|
+
const tail = (qualifier ? qualifier.replace(/::|\\/g, '.') + '.' : '') + name;
|
|
68
|
+
for (let n = parts.length; n >= 0; n--) {
|
|
69
|
+
const want = (n ? parts.slice(0, n).join('.') + '.' : '') + tail;
|
|
70
|
+
const hits = cands.filter(s => s.qname === want);
|
|
71
|
+
if (hits.length) return hits.find(s => !isTestPath(s.path)) ?? hits[0]!;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Types that directly extend / implement `sym`. Heritage is recorded by
|
|
78
|
+
* name, so each candidate is checked by resolving that name from the
|
|
79
|
+
* subtype's file (imports, same file, proximity) back to `sym`.
|
|
80
|
+
*/
|
|
81
|
+
export function directSubtypes(db: ClastDb, resolver: Resolver, sym: SymbolRow): TypeEdge[] {
|
|
82
|
+
if (isStaticClass(sym)) return [];
|
|
83
|
+
type Row = SymbolRow & { hkind: string; hqual: string | null; harity: number; hfile: number };
|
|
84
|
+
const symArity = sym.lang === 'csharp' ? declaredArity(sym) : -1;
|
|
85
|
+
const select = `SELECT ${SYMBOL_COLUMNS}, h.kind AS hkind, h.qualifier AS hqual, h.arity AS harity, h.file_id AS hfile FROM heritage h
|
|
86
|
+
JOIN symbols s ON s.id = h.symbol_id JOIN files f ON f.id = s.file_id`;
|
|
87
|
+
const rows = db.all<Row>(`${select} WHERE h.name = ? LIMIT 5000`, sym.name);
|
|
88
|
+
const sameName = db.symbolsByName(sym.name).filter(s => CONTAINER_KINDS.has(s.kind) || s.kind === 'type');
|
|
89
|
+
const out: TypeEdge[] = [];
|
|
90
|
+
const seen = new Set<number>();
|
|
91
|
+
|
|
92
|
+
// Subtypes that name the supertype through an import alias (`import { X as Base }`).
|
|
93
|
+
for (const use of resolver.aliasesOf(sym)) {
|
|
94
|
+
for (const row of db.all<Row>(`${select} WHERE h.name = ? AND h.file_id = ?`, use.alias, use.fileId)) {
|
|
95
|
+
if (row.id === sym.id || seen.has(row.id)) continue;
|
|
96
|
+
seen.add(row.id);
|
|
97
|
+
out.push({ sym: row, kind: row.hkind, certain: true });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const qparts = sym.qname.split('.');
|
|
102
|
+
const symContainer = qparts.length >= 2 ? qparts[qparts.length - 2]! : null;
|
|
103
|
+
for (const row of rows) {
|
|
104
|
+
if (row.id === sym.id || seen.has(row.id)) continue;
|
|
105
|
+
// A C++ class does not implement a C# interface of the same name.
|
|
106
|
+
if (langFamily(row.lang) !== langFamily(sym.lang)) continue;
|
|
107
|
+
// `io.Closer` / a `Wake` imported from std is not the project's type of that name.
|
|
108
|
+
if (resolver.externalSupertype(sym.name, row.hqual, row.hfile, row.lang)) continue;
|
|
109
|
+
// C#: `Foo` and `Foo<T>` are different types.
|
|
110
|
+
if (symArity !== -1 && row.harity !== symArity) continue;
|
|
111
|
+
let certain = true;
|
|
112
|
+
const lex = row.lang === 'ruby' ? lexicalType(db, row, row.hqual, sym.name) : null;
|
|
113
|
+
if (lex) {
|
|
114
|
+
// Ruby constant lookup is lexical: the name means the nearest enclosing definition.
|
|
115
|
+
if (lex.id !== sym.id && lex.qname !== sym.qname) continue;
|
|
116
|
+
} else if (row.lang === 'ruby' && !row.hqual && sym.qname.includes('.')) {
|
|
117
|
+
continue; // the unqualified name does not reach this nested constant from there
|
|
118
|
+
} else if (row.hqual && !qualifierFits(row.hqual, sym, row.lang)) {
|
|
119
|
+
continue; // Go / Rust: qualified by another package or module
|
|
120
|
+
} else if (row.hqual && symContainer) {
|
|
121
|
+
// Written as Outer::Name / Outer.Name: the qualifier must be our enclosing type or namespace.
|
|
122
|
+
if (row.hqual !== symContainer) continue;
|
|
123
|
+
} else if (sameName.length > 1 || row.hqual) {
|
|
124
|
+
const res = resolver.resolve(
|
|
125
|
+
sym.name,
|
|
126
|
+
{ fileId: row.file_id, path: row.path, lang: row.lang, receiver: row.hqual, caller: row },
|
|
127
|
+
0,
|
|
128
|
+
true
|
|
129
|
+
);
|
|
130
|
+
// A type does not extend itself (`class Foo<T> : Foo`), and in C# the arity must match.
|
|
131
|
+
const cands = res.candidates.filter(
|
|
132
|
+
c => c.sym.id !== row.id && (symArity === -1 || declaredArity(c.sym) === row.harity) && !isStaticClass(c.sym)
|
|
133
|
+
);
|
|
134
|
+
const top = cands[0];
|
|
135
|
+
if (!top) {
|
|
136
|
+
certain = false;
|
|
137
|
+
} else if (top.sym.id !== sym.id) {
|
|
138
|
+
const mine = cands.find(c => c.sym.id === sym.id);
|
|
139
|
+
// The heritage name clearly refers to another definition of the same name.
|
|
140
|
+
if (!mine || top.score - mine.score > 10) continue;
|
|
141
|
+
certain = false;
|
|
142
|
+
} else {
|
|
143
|
+
// C#: the arity already singled out one of the same-named types.
|
|
144
|
+
certain = res.confident || (symArity !== -1 && cands.length === 1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
seen.add(row.id);
|
|
148
|
+
out.push({ sym: row, kind: row.hkind, certain });
|
|
149
|
+
}
|
|
150
|
+
out.sort(
|
|
151
|
+
(a, b) =>
|
|
152
|
+
Number(b.certain) - Number(a.certain) ||
|
|
153
|
+
Number(isTestPath(a.sym.path)) - Number(isTestPath(b.sym.path)) ||
|
|
154
|
+
Number(b.sym.file_id === sym.file_id) - Number(a.sym.file_id === sym.file_id) ||
|
|
155
|
+
a.sym.path.localeCompare(b.sym.path) ||
|
|
156
|
+
a.sym.start_line - b.sym.start_line
|
|
157
|
+
);
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Declared supertypes of `sym`, resolved to project symbols where possible. */
|
|
162
|
+
export function directSupertypes(db: ClastDb, resolver: Resolver, sym: SymbolRow): SuperEdge[] {
|
|
163
|
+
// C# partial classes: the base list may be on any of the parts.
|
|
164
|
+
let ids = [sym.id];
|
|
165
|
+
if (sym.lang === 'csharp' && isPartial(sym)) {
|
|
166
|
+
const dir = sym.path.slice(0, sym.path.lastIndexOf('/') + 1);
|
|
167
|
+
ids = db
|
|
168
|
+
.symbolsByName(sym.name)
|
|
169
|
+
.filter(s => s.qname === sym.qname && s.kind === sym.kind && s.lang === sym.lang && isPartial(s) && s.path.startsWith(dir))
|
|
170
|
+
.map(s => s.id);
|
|
171
|
+
if (!ids.includes(sym.id)) ids.push(sym.id);
|
|
172
|
+
}
|
|
173
|
+
const rows = db.all<{ name: string; kind: string; qualifier: string | null; arity: number; file_id: number }>(
|
|
174
|
+
`SELECT name, kind, qualifier, arity, file_id FROM heritage WHERE symbol_id IN (${ids.map(() => '?').join(',')})`, ...ids
|
|
175
|
+
);
|
|
176
|
+
const out: SuperEdge[] = [];
|
|
177
|
+
const seen = new Set<string>();
|
|
178
|
+
for (const row of rows) {
|
|
179
|
+
// (`include Thor::Actions` and `include Rails::Generators::Actions` are two supertypes.)
|
|
180
|
+
const key = `${row.qualifier ?? ''}::${row.name}`;
|
|
181
|
+
if (seen.has(key)) continue;
|
|
182
|
+
seen.add(key);
|
|
183
|
+
const isType = (s: SymbolRow): boolean =>
|
|
184
|
+
(CONTAINER_KINDS.has(s.kind) || s.kind === 'type') && s.id !== sym.id && !isStaticClass(s) &&
|
|
185
|
+
// Rust associated types (`type Future = ...` inside an impl) are not the trait `Future`.
|
|
186
|
+
!(s.lang === 'rust' && s.kind === 'type' && s.qname !== s.name);
|
|
187
|
+
if (resolver.externalSupertype(row.name, row.qualifier, row.file_id, sym.lang)) {
|
|
188
|
+
out.push({ name: row.name, kind: row.kind, sym: null });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
let res = resolver.resolve(
|
|
192
|
+
row.name,
|
|
193
|
+
{ fileId: sym.file_id, path: sym.path, lang: sym.lang, receiver: row.qualifier, caller: sym },
|
|
194
|
+
0,
|
|
195
|
+
true
|
|
196
|
+
);
|
|
197
|
+
let target = res.candidates.find(c => isType(c.sym));
|
|
198
|
+
if (sym.lang === 'ruby' || !res.confident) {
|
|
199
|
+
const lex = lexicalType(db, sym, row.qualifier, row.name);
|
|
200
|
+
if (lex) target = { sym: lex, score: 95 };
|
|
201
|
+
// Ruby: an unqualified constant that no enclosing scope defines is not some class of that name
|
|
202
|
+
// nested elsewhere (`class SlowRedis < Redis` at top level means the gem's Redis).
|
|
203
|
+
// The same holds for a qualified one: `Thor::Group` is not some other class called Group.
|
|
204
|
+
else if (sym.lang === 'ruby') target = undefined;
|
|
205
|
+
}
|
|
206
|
+
if (!target && row.qualifier && sym.lang !== 'ruby') {
|
|
207
|
+
// Qualified by an enclosing type / namespace that is part of the qualified name.
|
|
208
|
+
const want = row.qualifier + '.' + row.name;
|
|
209
|
+
const direct = db.symbolsByName(row.name).find(s => isType(s) && (s.qname === want || s.qname.endsWith('.' + want)));
|
|
210
|
+
if (direct) target = { sym: direct, score: 90 };
|
|
211
|
+
else {
|
|
212
|
+
res = resolver.resolve(
|
|
213
|
+
row.name, { fileId: sym.file_id, path: sym.path, lang: sym.lang, receiver: null, caller: sym }, 0, true
|
|
214
|
+
);
|
|
215
|
+
target = res.candidates.find(c => isType(c.sym) && qualifierFits(row.qualifier!, c.sym, sym.lang));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Go / Rust: `page.Provider`, `futures_io::AsyncRead` name a package / module. A same-named type
|
|
219
|
+
// that lives somewhere else is a different type.
|
|
220
|
+
if (target && row.qualifier && !qualifierFits(row.qualifier, target.sym, sym.lang)) {
|
|
221
|
+
const better = db.symbolsByName(row.name).find(s => isType(s) && s.lang === sym.lang && qualifierFits(row.qualifier!, s, sym.lang, true));
|
|
222
|
+
target = better ? { sym: better, score: 90 } : undefined;
|
|
223
|
+
}
|
|
224
|
+
if (target && sym.lang === 'csharp' && declaredArity(target.sym) !== row.arity) {
|
|
225
|
+
// `: ActionResult` is not `ActionResult<TValue>`.
|
|
226
|
+
const fit = db.symbolsByName(row.name).filter(s => isType(s) && s.lang === 'csharp' && declaredArity(s) === row.arity);
|
|
227
|
+
const best = fit.find(s => !isTestPath(s.path)) ?? fit[0];
|
|
228
|
+
target = best ? { sym: best, score: 90 } : undefined;
|
|
229
|
+
}
|
|
230
|
+
// PHP traits are recorded as 'implements'; say what they are.
|
|
231
|
+
const kind = target && target.sym.kind === 'trait' && sym.lang === 'php' ? 'uses' : row.kind;
|
|
232
|
+
out.push({ name: row.name, kind, sym: target ? target.sym : null });
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
function dirOf(path: string): string {
|
|
239
|
+
const i = path.lastIndexOf('/');
|
|
240
|
+
return i === -1 ? '' : path.slice(0, i);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Top-level comma-separated items in a parameter / result list. */
|
|
244
|
+
function countItems(list: string): number {
|
|
245
|
+
if (!list.trim()) return 0;
|
|
246
|
+
let depth = 0;
|
|
247
|
+
let n = 1;
|
|
248
|
+
for (const ch of list) {
|
|
249
|
+
if (ch === '(' || ch === '[' || ch === '{') depth++;
|
|
250
|
+
else if (ch === ')' || ch === ']' || ch === '}') depth--;
|
|
251
|
+
else if (ch === ',' && depth === 0) n++;
|
|
252
|
+
}
|
|
253
|
+
return n;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Shape of a Go method signature: number of parameters and of results
|
|
258
|
+
* (`Close() error` -> "0/1", `Close(n Node, r Reader)` -> "2/0"). Go matches
|
|
259
|
+
* interfaces on full signatures; counting is enough to reject look-alikes
|
|
260
|
+
* without parsing types. Null when the signature cannot be read.
|
|
261
|
+
*/
|
|
262
|
+
export function goShape(signature: string, name: string): string | null {
|
|
263
|
+
let sig = signature;
|
|
264
|
+
// func (r *T) Name(...) ...: skip the receiver.
|
|
265
|
+
const recv = /^func\s*\(/.exec(sig);
|
|
266
|
+
if (recv) {
|
|
267
|
+
let depth = 0;
|
|
268
|
+
let i = recv[0].length - 1;
|
|
269
|
+
for (; i < sig.length; i++) {
|
|
270
|
+
if (sig[i] === '(') depth++;
|
|
271
|
+
else if (sig[i] === ')' && --depth === 0) break;
|
|
272
|
+
}
|
|
273
|
+
sig = sig.slice(i + 1);
|
|
274
|
+
}
|
|
275
|
+
const at = sig.indexOf(name);
|
|
276
|
+
if (at === -1) return null;
|
|
277
|
+
let i = at + name.length;
|
|
278
|
+
while (i < sig.length && sig[i] === ' ') i++;
|
|
279
|
+
if (sig[i] === '[') {
|
|
280
|
+
// type parameters
|
|
281
|
+
let depth = 0;
|
|
282
|
+
for (; i < sig.length; i++) {
|
|
283
|
+
if (sig[i] === '[') depth++;
|
|
284
|
+
else if (sig[i] === ']' && --depth === 0) { i++; break; }
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (sig[i] !== '(') return null;
|
|
288
|
+
let depth = 0;
|
|
289
|
+
const open = i;
|
|
290
|
+
for (; i < sig.length; i++) {
|
|
291
|
+
if (sig[i] === '(') depth++;
|
|
292
|
+
else if (sig[i] === ')' && --depth === 0) break;
|
|
293
|
+
}
|
|
294
|
+
if (i >= sig.length) return null; // cut off
|
|
295
|
+
const params = countItems(sig.slice(open + 1, i));
|
|
296
|
+
const rest = sig.slice(i + 1).replace(/\{\s*$/, '').trim();
|
|
297
|
+
let results = 0;
|
|
298
|
+
if (rest.startsWith('(')) {
|
|
299
|
+
const close = rest.lastIndexOf(')');
|
|
300
|
+
if (close === -1) return null;
|
|
301
|
+
results = countItems(rest.slice(1, close));
|
|
302
|
+
} else if (rest) {
|
|
303
|
+
results = 1;
|
|
304
|
+
}
|
|
305
|
+
return params + '/' + results;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
interface Member {
|
|
309
|
+
id: number;
|
|
310
|
+
name: string;
|
|
311
|
+
shape: string | null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function membersOf(db: ClastDb, containerId: number): Member[] {
|
|
315
|
+
const seen = new Set<string>();
|
|
316
|
+
const out: Member[] = [];
|
|
317
|
+
for (const r of db.all<{ id: number; name: string; signature: string }>(
|
|
318
|
+
`SELECT id, name, signature FROM symbols WHERE parent_id = ? AND kind = 'method'`, containerId
|
|
319
|
+
)) {
|
|
320
|
+
if (seen.has(r.name)) continue;
|
|
321
|
+
seen.add(r.name);
|
|
322
|
+
out.push({ id: r.id, name: r.name, shape: goShape(r.signature, r.name) });
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Methods of the type called `typeName` that are defined outside its body
|
|
329
|
+
* (Go receivers), with their shapes. `dir` limits this to one package.
|
|
330
|
+
*/
|
|
331
|
+
function methodsOfType(db: ClastDb, typeName: string, lang: string, dir?: string): Map<string, Set<string | null>> {
|
|
332
|
+
const rows = db.all<{ name: string; qname: string; signature: string; path: string }>(
|
|
333
|
+
`SELECT s.name, s.qname, s.signature, f.path FROM symbols s JOIN files f ON f.id = s.file_id
|
|
334
|
+
WHERE s.kind = 'method' AND f.lang = ? AND s.qname LIKE ? ESCAPE '\\'`,
|
|
335
|
+
lang, '%' + typeName.replace(/[\\%_]/g, c => '\\' + c) + '.%'
|
|
336
|
+
);
|
|
337
|
+
const out = new Map<string, Set<string | null>>();
|
|
338
|
+
for (const r of rows) {
|
|
339
|
+
const parts = r.qname.split('.');
|
|
340
|
+
if (parts.length < 2 || parts[parts.length - 2] !== typeName) continue;
|
|
341
|
+
if (dir !== undefined && dirOf(r.path) !== dir) continue;
|
|
342
|
+
let set = out.get(r.name);
|
|
343
|
+
if (!set) out.set(r.name, (set = new Set()));
|
|
344
|
+
set.add(goShape(r.signature, r.name));
|
|
345
|
+
}
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Does a method set cover every required interface method (by name and, where readable, shape)? */
|
|
350
|
+
function covers(methods: Map<string, Set<string | null>>, required: Member[]): boolean {
|
|
351
|
+
for (const m of required) {
|
|
352
|
+
const shapes = methods.get(m.name);
|
|
353
|
+
if (!shapes) return false;
|
|
354
|
+
if (m.shape !== null && !shapes.has(m.shape) && !shapes.has(null)) return false;
|
|
355
|
+
}
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Go has no `implements`: a type satisfies an interface when its method set
|
|
361
|
+
* contains the interface's methods. Returns the types whose methods (name,
|
|
362
|
+
* parameter and result counts) cover every method of the interface.
|
|
363
|
+
*/
|
|
364
|
+
export function structuralImplementers(db: ClastDb, iface: SymbolRow, limit = 200): SymbolRow[] {
|
|
365
|
+
if (iface.lang !== 'go' || iface.kind !== 'interface') return [];
|
|
366
|
+
const required = membersOf(db, iface.id);
|
|
367
|
+
if (required.length === 0) return [];
|
|
368
|
+
|
|
369
|
+
// Start from the rarest method name to keep the candidate set small.
|
|
370
|
+
let rarest = required[0]!.name;
|
|
371
|
+
let rarestCount = Infinity;
|
|
372
|
+
for (const m of required) {
|
|
373
|
+
const n = db.get<{ n: number }>(`SELECT COUNT(*) AS n FROM symbols WHERE name = ? AND kind = 'method'`, m.name)?.n ?? 0;
|
|
374
|
+
if (n < rarestCount) { rarestCount = n; rarest = m.name; }
|
|
375
|
+
}
|
|
376
|
+
const holders = db.all<{ qname: string; path: string }>(
|
|
377
|
+
`SELECT s.qname, f.path FROM symbols s JOIN files f ON f.id = s.file_id
|
|
378
|
+
WHERE s.name = ? AND s.kind = 'method' AND f.lang = 'go' LIMIT 5000`,
|
|
379
|
+
rarest
|
|
380
|
+
);
|
|
381
|
+
// A type is identified by its package directory and name.
|
|
382
|
+
const types = new Map<string, { dir: string; name: string }>();
|
|
383
|
+
for (const h of holders) {
|
|
384
|
+
const parts = h.qname.split('.');
|
|
385
|
+
if (parts.length < 2) continue;
|
|
386
|
+
const name = parts[parts.length - 2]!;
|
|
387
|
+
const dir = dirOf(h.path);
|
|
388
|
+
if (name === iface.name && dir === dirOf(iface.path)) continue;
|
|
389
|
+
types.set(dir + '\0' + name, { dir, name });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const out: SymbolRow[] = [];
|
|
393
|
+
for (const t of types.values()) {
|
|
394
|
+
if (!covers(methodsOfType(db, t.name, 'go', t.dir), required)) continue;
|
|
395
|
+
const defs = db
|
|
396
|
+
.all<SymbolRow>(`SELECT ${SYMBOL_COLUMNS} FROM ${SYMBOL_FROM} WHERE s.name = ? AND f.lang = 'go'`, t.name)
|
|
397
|
+
.filter(s => (s.kind === 'struct' || s.kind === 'type') && dirOf(s.path) === t.dir);
|
|
398
|
+
for (const d of defs) out.push(d);
|
|
399
|
+
if (out.length >= limit) break;
|
|
400
|
+
}
|
|
401
|
+
out.sort((a, b) => Number(isTestPath(a.path)) - Number(isTestPath(b.path)) || a.path.localeCompare(b.path));
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Go: interfaces whose method set is covered by the methods of `type`. */
|
|
406
|
+
export function structuralInterfaces(db: ClastDb, type: SymbolRow, limit = 40): SymbolRow[] {
|
|
407
|
+
if (type.lang !== 'go' || (type.kind !== 'struct' && type.kind !== 'type')) return [];
|
|
408
|
+
const methods = methodsOfType(db, type.name, 'go', dirOf(type.path));
|
|
409
|
+
if (methods.size === 0) return [];
|
|
410
|
+
const ifaces = db.all<SymbolRow>(
|
|
411
|
+
`SELECT ${SYMBOL_COLUMNS} FROM ${SYMBOL_FROM} WHERE s.kind = 'interface' AND f.lang = 'go' LIMIT 20000`
|
|
412
|
+
);
|
|
413
|
+
const out: SymbolRow[] = [];
|
|
414
|
+
for (const iface of ifaces) {
|
|
415
|
+
const required = membersOf(db, iface.id);
|
|
416
|
+
if (required.length === 0 || required.length > methods.size) continue;
|
|
417
|
+
if (covers(methods, required)) out.push(iface);
|
|
418
|
+
if (out.length >= limit) break;
|
|
419
|
+
}
|
|
420
|
+
out.sort((a, b) => Number(isTestPath(a.path)) - Number(isTestPath(b.path)) || a.path.localeCompare(b.path));
|
|
421
|
+
return out;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Go: ids of the interface methods that `method` implements (its type covers
|
|
426
|
+
* the whole interface). A call through such an interface may reach `method`.
|
|
427
|
+
*/
|
|
428
|
+
export function goInterfaceMethods(db: ClastDb, method: SymbolRow): number[] {
|
|
429
|
+
if (method.lang !== 'go' || method.kind !== 'method') return [];
|
|
430
|
+
const parts = method.qname.split('.');
|
|
431
|
+
if (parts.length < 2) return [];
|
|
432
|
+
const typeName = parts[parts.length - 2]!;
|
|
433
|
+
const decls = db.all<{ id: number; parent_id: number }>(
|
|
434
|
+
`SELECT s.id, s.parent_id FROM symbols s JOIN symbols p ON p.id = s.parent_id JOIN files f ON f.id = s.file_id
|
|
435
|
+
WHERE s.name = ? AND s.kind = 'method' AND p.kind = 'interface' AND f.lang = 'go' LIMIT 400`,
|
|
436
|
+
method.name
|
|
437
|
+
);
|
|
438
|
+
if (decls.length === 0) return [];
|
|
439
|
+
const methods = methodsOfType(db, typeName, 'go', dirOf(method.path));
|
|
440
|
+
const out: number[] = [];
|
|
441
|
+
for (const d of decls) {
|
|
442
|
+
if (d.id === method.id) continue;
|
|
443
|
+
const required = membersOf(db, d.parent_id);
|
|
444
|
+
if (required.length && covers(methods, required)) out.push(d.id);
|
|
445
|
+
}
|
|
446
|
+
return out;
|
|
447
|
+
}
|