@crouton-kit/tsym 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/bin/tsym.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +89 -0
- package/dist/commands/handlers/check.d.ts +3 -0
- package/dist/commands/handlers/check.js +94 -0
- package/dist/commands/handlers/find.d.ts +3 -0
- package/dist/commands/handlers/find.js +56 -0
- package/dist/commands/handlers/inspect.d.ts +3 -0
- package/dist/commands/handlers/inspect.js +81 -0
- package/dist/commands/handlers/outline.d.ts +3 -0
- package/dist/commands/handlers/outline.js +101 -0
- package/dist/commands/handlers/query.d.ts +3 -0
- package/dist/commands/handlers/query.js +196 -0
- package/dist/commands/handlers/rename.d.ts +3 -0
- package/dist/commands/handlers/rename.js +53 -0
- package/dist/commands/handlers/search.d.ts +5 -0
- package/dist/commands/handlers/search.js +32 -0
- package/dist/commands/handlers/server-reload.d.ts +3 -0
- package/dist/commands/handlers/server-reload.js +7 -0
- package/dist/commands/handlers/server-status.d.ts +3 -0
- package/dist/commands/handlers/server-status.js +7 -0
- package/dist/commands/handlers/server-stop.d.ts +3 -0
- package/dist/commands/handlers/server-stop.js +7 -0
- package/dist/commands/handlers/show.d.ts +3 -0
- package/dist/commands/handlers/show.js +63 -0
- package/dist/commands/handlers/type.d.ts +3 -0
- package/dist/commands/handlers/type.js +45 -0
- package/dist/commands/handlers.d.ts +12 -0
- package/dist/commands/handlers.js +44 -0
- package/dist/commands/help.d.ts +2 -0
- package/dist/commands/help.js +27 -0
- package/dist/commands/rename-apply.d.ts +37 -0
- package/dist/commands/rename-apply.js +115 -0
- package/dist/commands/tree.d.ts +9 -0
- package/dist/commands/tree.js +277 -0
- package/dist/commands/types.d.ts +80 -0
- package/dist/commands/types.js +1 -0
- package/dist/core/declarations.d.ts +48 -0
- package/dist/core/declarations.js +153 -0
- package/dist/core/receipt.d.ts +7 -0
- package/dist/core/receipt.js +9 -0
- package/dist/core/search.d.ts +29 -0
- package/dist/core/search.js +47 -0
- package/dist/core/source.d.ts +32 -0
- package/dist/core/source.js +96 -0
- package/dist/core/workspace.d.ts +28 -0
- package/dist/core/workspace.js +137 -0
- package/dist/index/build.d.ts +22 -0
- package/dist/index/build.js +79 -0
- package/dist/index/classify.d.ts +20 -0
- package/dist/index/classify.js +60 -0
- package/dist/index/commits.d.ts +15 -0
- package/dist/index/commits.js +201 -0
- package/dist/index/descriptor.d.ts +19 -0
- package/dist/index/descriptor.js +295 -0
- package/dist/index/graph.d.ts +82 -0
- package/dist/index/graph.js +1 -0
- package/dist/index/heritage.d.ts +15 -0
- package/dist/index/heritage.js +122 -0
- package/dist/index/refresh.d.ts +28 -0
- package/dist/index/refresh.js +110 -0
- package/dist/index/scope.d.ts +4 -0
- package/dist/index/scope.js +21 -0
- package/dist/index/state.d.ts +22 -0
- package/dist/index/state.js +63 -0
- package/dist/index/walk.d.ts +23 -0
- package/dist/index/walk.js +550 -0
- package/dist/index/watch.d.ts +18 -0
- package/dist/index/watch.js +175 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/output.d.ts +12 -0
- package/dist/output.js +54 -0
- package/dist/read/read.d.ts +32 -0
- package/dist/read/read.js +89 -0
- package/dist/relations/graph.d.ts +23 -0
- package/dist/relations/graph.js +115 -0
- package/dist/relations/mermaid.d.ts +3 -0
- package/dist/relations/mermaid.js +37 -0
- package/dist/relations/store-relations.d.ts +20 -0
- package/dist/relations/store-relations.js +158 -0
- package/dist/relations/types.d.ts +58 -0
- package/dist/relations/types.js +12 -0
- package/dist/server/client.d.ts +5 -0
- package/dist/server/client.js +215 -0
- package/dist/server/context.d.ts +21 -0
- package/dist/server/context.js +12 -0
- package/dist/server/lifecycle.d.ts +28 -0
- package/dist/server/lifecycle.js +62 -0
- package/dist/server/main.d.ts +1 -0
- package/dist/server/main.js +541 -0
- package/dist/server/projects.d.ts +21 -0
- package/dist/server/projects.js +28 -0
- package/dist/server/wire.d.ts +39 -0
- package/dist/server/wire.js +19 -0
- package/dist/store/load.d.ts +67 -0
- package/dist/store/load.js +193 -0
- package/dist/store/meta.d.ts +28 -0
- package/dist/store/meta.js +137 -0
- package/dist/store/query.d.ts +5 -0
- package/dist/store/query.js +4 -0
- package/dist/store/schema.d.ts +4 -0
- package/dist/store/schema.js +19 -0
- package/dist/store/store.d.ts +36 -0
- package/dist/store/store.js +240 -0
- package/dist/ts7/api.d.ts +35 -0
- package/dist/ts7/api.js +164 -0
- package/dist/ts7/ast.d.ts +1 -0
- package/dist/ts7/ast.js +1 -0
- package/dist/ts7/checker.d.ts +55 -0
- package/dist/ts7/checker.js +383 -0
- package/dist/ts7/projects.d.ts +27 -0
- package/dist/ts7/projects.js +94 -0
- package/package.json +47 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type DeclarationKind = 'module' | 'function' | 'class' | 'interface' | 'type' | 'enum' | 'namespace' | 'const' | 'let' | 'var' | 'method' | 'property' | 'getter' | 'setter' | 'constructor';
|
|
2
|
+
export interface DeclarationNode {
|
|
3
|
+
key: string;
|
|
4
|
+
at: string;
|
|
5
|
+
name: string;
|
|
6
|
+
qname: string;
|
|
7
|
+
kind: DeclarationKind;
|
|
8
|
+
exported: boolean;
|
|
9
|
+
file: string;
|
|
10
|
+
dir: string;
|
|
11
|
+
top: string;
|
|
12
|
+
line: number;
|
|
13
|
+
col: number;
|
|
14
|
+
endLine: number;
|
|
15
|
+
lines: number;
|
|
16
|
+
declCount: number;
|
|
17
|
+
container: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface ExternalNode {
|
|
20
|
+
key: string;
|
|
21
|
+
module: string;
|
|
22
|
+
name: string;
|
|
23
|
+
}
|
|
24
|
+
export type ReferenceRole = 'definition' | 'call' | 'import' | 'type' | 'write' | 'read';
|
|
25
|
+
export type TypePosition = 'parameter' | 'return' | 'property' | 'variable' | 'type';
|
|
26
|
+
export interface ContainsRelationship {
|
|
27
|
+
type: 'CONTAINS';
|
|
28
|
+
from: string;
|
|
29
|
+
to: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ReferenceRelationship {
|
|
32
|
+
type: 'REFERENCES';
|
|
33
|
+
from: string;
|
|
34
|
+
to: string;
|
|
35
|
+
line: number;
|
|
36
|
+
col: number;
|
|
37
|
+
role: ReferenceRole;
|
|
38
|
+
}
|
|
39
|
+
export interface CallRelationship {
|
|
40
|
+
type: 'CALLS';
|
|
41
|
+
from: string;
|
|
42
|
+
to: string;
|
|
43
|
+
line: number;
|
|
44
|
+
col: number;
|
|
45
|
+
dispatch: boolean;
|
|
46
|
+
}
|
|
47
|
+
export interface TypeMentionRelationship {
|
|
48
|
+
type: 'TYPE_MENTION';
|
|
49
|
+
from: string;
|
|
50
|
+
to: string;
|
|
51
|
+
line: number;
|
|
52
|
+
col: number;
|
|
53
|
+
position: TypePosition;
|
|
54
|
+
}
|
|
55
|
+
export interface ImportRelationship {
|
|
56
|
+
type: 'IMPORTS';
|
|
57
|
+
from: string;
|
|
58
|
+
to: string;
|
|
59
|
+
line: number;
|
|
60
|
+
names: string;
|
|
61
|
+
}
|
|
62
|
+
export interface ExportRelationship {
|
|
63
|
+
type: 'EXPORTS';
|
|
64
|
+
from: string;
|
|
65
|
+
to: string;
|
|
66
|
+
name: string;
|
|
67
|
+
line: number;
|
|
68
|
+
col: number;
|
|
69
|
+
}
|
|
70
|
+
export type IndexRelationship = ContainsRelationship | ReferenceRelationship | CallRelationship | TypeMentionRelationship | ImportRelationship | ExportRelationship;
|
|
71
|
+
export interface FileGraph {
|
|
72
|
+
declarations: readonly string[];
|
|
73
|
+
relationships: readonly IndexRelationship[];
|
|
74
|
+
}
|
|
75
|
+
export interface IndexGraph {
|
|
76
|
+
declarations: ReadonlyMap<string, DeclarationNode>;
|
|
77
|
+
externals: ReadonlyMap<string, ExternalNode>;
|
|
78
|
+
files: ReadonlyMap<string, FileGraph>;
|
|
79
|
+
relationships: readonly IndexRelationship[];
|
|
80
|
+
internalSites: number;
|
|
81
|
+
unresolvedOccurrences: number;
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Ts7Host } from '../ts7/api.js';
|
|
2
|
+
import { ConfiguredProjects } from '../ts7/projects.js';
|
|
3
|
+
import type { IndexGraph } from './graph.js';
|
|
4
|
+
export interface HeritageRelationship {
|
|
5
|
+
type: 'EXTENDS' | 'IMPLEMENTS';
|
|
6
|
+
from: string;
|
|
7
|
+
to: string;
|
|
8
|
+
}
|
|
9
|
+
export interface HeritageGraph {
|
|
10
|
+
graph: IndexGraph;
|
|
11
|
+
relationships: readonly HeritageRelationship[];
|
|
12
|
+
dispatchCalls: number;
|
|
13
|
+
}
|
|
14
|
+
/** Adds compiler-resolved heritage edges and marks calls that require implementation dispatch. */
|
|
15
|
+
export declare function addHeritage(root: string, program: Ts7Host, projects: ConfiguredProjects, graph: IndexGraph): HeritageGraph;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { SymbolFlags } from 'typescript/unstable/sync';
|
|
2
|
+
import * as ts from '../ts7/ast.js';
|
|
3
|
+
import { isIndexedDeclaration } from './scope.js';
|
|
4
|
+
import { relative } from './classify.js';
|
|
5
|
+
import { walkFileStates } from './walk.js';
|
|
6
|
+
/** Adds compiler-resolved heritage edges and marks calls that require implementation dispatch. */
|
|
7
|
+
export function addHeritage(root, program, projects, graph) {
|
|
8
|
+
const states = walkFileStates(graph);
|
|
9
|
+
const stateByFile = new Map(states.map((state) => [state.file, state]));
|
|
10
|
+
const declarationsByKey = new Map();
|
|
11
|
+
for (const state of states)
|
|
12
|
+
for (const descriptor of state.descriptors) {
|
|
13
|
+
if (!graph.declarations.has(descriptor.key))
|
|
14
|
+
continue;
|
|
15
|
+
const node = state.nodesByStart.get(descriptor.start);
|
|
16
|
+
if (!node)
|
|
17
|
+
continue;
|
|
18
|
+
const nodes = declarationsByKey.get(descriptor.key) ?? [];
|
|
19
|
+
nodes.push(node);
|
|
20
|
+
declarationsByKey.set(descriptor.key, nodes);
|
|
21
|
+
}
|
|
22
|
+
const targetKey = (checker, symbol) => {
|
|
23
|
+
if (!symbol)
|
|
24
|
+
return undefined;
|
|
25
|
+
const resolved = symbol.flags & SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
|
|
26
|
+
for (const handle of resolved.declarations) {
|
|
27
|
+
const declaration = handle.resolve();
|
|
28
|
+
if (!declaration)
|
|
29
|
+
continue;
|
|
30
|
+
const file = relative(root, declaration.getSourceFile().fileName);
|
|
31
|
+
const state = stateByFile.get(file);
|
|
32
|
+
const descriptor = state?.byStart.get(declaration.getStart(declaration.getSourceFile()));
|
|
33
|
+
if (descriptor && graph.declarations.has(descriptor.key))
|
|
34
|
+
return descriptor.key;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
};
|
|
38
|
+
const relationships = new Map();
|
|
39
|
+
const add = (relationship) => { relationships.set(`${relationship.type}:${relationship.from}:${relationship.to}`, relationship); };
|
|
40
|
+
for (const state of states) {
|
|
41
|
+
const visit = (node) => {
|
|
42
|
+
if (ts.isClassDeclaration(node) || ts.isClassExpression(node) || ts.isInterfaceDeclaration(node)) {
|
|
43
|
+
const source = state.byStart.get(node.getStart(state.source));
|
|
44
|
+
if (source && graph.declarations.has(source.key)) {
|
|
45
|
+
for (const clause of node.heritageClauses ?? []) {
|
|
46
|
+
for (const type of clause.types) {
|
|
47
|
+
const target = targetKey(state.checker, state.checker.getSymbolAtLocation(type.expression));
|
|
48
|
+
if (!target)
|
|
49
|
+
continue;
|
|
50
|
+
if (clause.token === ts.SyntaxKind.ImplementsKeyword && (ts.isClassDeclaration(node) || ts.isClassExpression(node)))
|
|
51
|
+
add({ type: 'IMPLEMENTS', from: source.key, to: target });
|
|
52
|
+
else if (clause.token === ts.SyntaxKind.ExtendsKeyword)
|
|
53
|
+
add({ type: 'EXTENDS', from: source.key, to: target });
|
|
54
|
+
if (ts.isClassDeclaration(node) || ts.isClassExpression(node))
|
|
55
|
+
addMatchingMembers(node, state, target, targetKey, declarationsByKey, add);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
node.forEachChild(visit);
|
|
61
|
+
};
|
|
62
|
+
state.source.forEachChild(visit);
|
|
63
|
+
}
|
|
64
|
+
const dispatch = (target) => (declarationsByKey.get(target) ?? []).some(isDispatchTarget);
|
|
65
|
+
const decoratedRelationships = graph.relationships.map((relationship) => relationship.type === 'CALLS'
|
|
66
|
+
? { ...relationship, dispatch: dispatch(relationship.to) }
|
|
67
|
+
: relationship);
|
|
68
|
+
const decoratedByFile = new Map();
|
|
69
|
+
for (const relationship of decoratedRelationships) {
|
|
70
|
+
const file = graph.declarations.get(relationship.from)?.file;
|
|
71
|
+
if (file)
|
|
72
|
+
(decoratedByFile.get(file) ?? decoratedByFile.set(file, []).get(file)).push(relationship);
|
|
73
|
+
}
|
|
74
|
+
const files = new Map([...graph.files].map(([file, record]) => [file, { ...record, relationships: decoratedByFile.get(file) ?? [] }]));
|
|
75
|
+
const decorated = { ...graph, files, relationships: decoratedRelationships };
|
|
76
|
+
const dispatchCalls = decoratedRelationships.filter((relationship) => relationship.type === 'CALLS' && relationship.dispatch).length;
|
|
77
|
+
return { graph: decorated, relationships: [...relationships.values()], dispatchCalls };
|
|
78
|
+
}
|
|
79
|
+
function addMatchingMembers(node, state, target, targetKey, declarationsByKey, add) {
|
|
80
|
+
const targetDeclaration = declarationsByKey.get(target)?.find((candidate) => ts.isInterfaceDeclaration(candidate) || ts.isClassDeclaration(candidate));
|
|
81
|
+
if (!targetDeclaration?.name)
|
|
82
|
+
return;
|
|
83
|
+
const targetType = state.checker.getTypeAtLocation(targetDeclaration.name);
|
|
84
|
+
if (!targetType)
|
|
85
|
+
return;
|
|
86
|
+
for (const member of node.members) {
|
|
87
|
+
const descriptor = state.byStart.get(member.getStart(state.source));
|
|
88
|
+
const name = memberName(member);
|
|
89
|
+
if (!descriptor || !name || !isIndexedDeclaration(descriptor) || hasModifier(member, ts.SyntaxKind.AbstractKeyword))
|
|
90
|
+
continue;
|
|
91
|
+
const baseMember = state.checker.getPropertyOfType(targetType, name);
|
|
92
|
+
const memberTarget = targetKey(state.checker, baseMember);
|
|
93
|
+
const dispatch = baseMember?.declarations.some((handle) => {
|
|
94
|
+
const declaration = handle.resolve();
|
|
95
|
+
return declaration !== undefined && isDispatchTarget(declaration);
|
|
96
|
+
}) === true;
|
|
97
|
+
if (memberTarget && dispatch)
|
|
98
|
+
add({ type: 'IMPLEMENTS', from: descriptor.key, to: memberTarget });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function memberName(node) {
|
|
102
|
+
const name = node.name;
|
|
103
|
+
return name?.text;
|
|
104
|
+
}
|
|
105
|
+
function isDispatchTarget(node) {
|
|
106
|
+
if (ts.isMethodSignatureDeclaration(node))
|
|
107
|
+
return true;
|
|
108
|
+
if (!(ts.isMethodDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isVariableDeclaration(node)))
|
|
109
|
+
return false;
|
|
110
|
+
if (hasModifier(node, ts.SyntaxKind.AbstractKeyword) || hasModifier(node, ts.SyntaxKind.DeclareKeyword))
|
|
111
|
+
return true;
|
|
112
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
113
|
+
if (hasModifier(current, ts.SyntaxKind.DeclareKeyword))
|
|
114
|
+
return true;
|
|
115
|
+
if (ts.isSourceFile(current))
|
|
116
|
+
return current.fileName.endsWith('.d.ts');
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
function hasModifier(node, kind) {
|
|
121
|
+
return node.modifiers?.some((modifier) => modifier.kind === kind) === true;
|
|
122
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type StoreGraph } from '../store/load.js';
|
|
2
|
+
import { Store } from '../store/store.js';
|
|
3
|
+
import { Ts7Host } from '../ts7/api.js';
|
|
4
|
+
import { ConfiguredProjects } from '../ts7/projects.js';
|
|
5
|
+
import { type HeritageGraph } from './heritage.js';
|
|
6
|
+
import { type BuiltIndex } from './build.js';
|
|
7
|
+
export interface RefreshOptions {
|
|
8
|
+
fileChanges: {
|
|
9
|
+
changed?: string[];
|
|
10
|
+
created?: string[];
|
|
11
|
+
deleted?: string[];
|
|
12
|
+
} | {
|
|
13
|
+
invalidateAll: true;
|
|
14
|
+
};
|
|
15
|
+
full?: boolean;
|
|
16
|
+
validate?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface RefreshResult {
|
|
19
|
+
built: BuiltIndex;
|
|
20
|
+
changedFiles: readonly string[];
|
|
21
|
+
stored: 'full' | 'files' | 'none';
|
|
22
|
+
validation: 'off' | 'matched';
|
|
23
|
+
}
|
|
24
|
+
/** Updates the TS7 snapshot, conservatively re-walks the union, then writes only changed file records. */
|
|
25
|
+
export declare function refreshIndex(root: string, program: Ts7Host, projects: ConfiguredProjects, store: Store, previous: BuiltIndex, options: RefreshOptions): RefreshResult;
|
|
26
|
+
export declare function graphsEqual(left: HeritageGraph, right: HeritageGraph): boolean;
|
|
27
|
+
export declare function fileFingerprint(graph: BuiltIndex['graph'], heritage: readonly HeritageGraph['relationships'][number][], file: string): string;
|
|
28
|
+
export declare function graphFingerprint(graph: StoreGraph): string;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { loadGraph, replaceFiles } from '../store/load.js';
|
|
3
|
+
import { metadataFor, writeMetadata } from '../store/meta.js';
|
|
4
|
+
import { addHeritage } from './heritage.js';
|
|
5
|
+
import { storeGraph } from './build.js';
|
|
6
|
+
import { walkIndex } from './walk.js';
|
|
7
|
+
/** Updates the TS7 snapshot, conservatively re-walks the union, then writes only changed file records. */
|
|
8
|
+
export function refreshIndex(root, program, projects, store, previous, options) {
|
|
9
|
+
const started = performance.now();
|
|
10
|
+
// The update carries a rewritten config's new text, so it must precede the rediscovery that reads membership from it.
|
|
11
|
+
program.update({ fileChanges: options.fileChanges });
|
|
12
|
+
if (options.full)
|
|
13
|
+
projects.reload();
|
|
14
|
+
const candidate = indexGraph(root, program, projects);
|
|
15
|
+
const graph = storeGraph(candidate);
|
|
16
|
+
const changedFiles = differingFiles(previous, candidate);
|
|
17
|
+
let stored = 'none';
|
|
18
|
+
if (options.full && changedFiles.length > 0) {
|
|
19
|
+
loadGraph(store, graph);
|
|
20
|
+
stored = 'full';
|
|
21
|
+
}
|
|
22
|
+
else if (changedFiles.length > 0) {
|
|
23
|
+
replaceFiles(store, changedFiles, fileGraph(graph, changedFiles));
|
|
24
|
+
stored = 'files';
|
|
25
|
+
}
|
|
26
|
+
const durationMs = performance.now() - started;
|
|
27
|
+
writeMetadata(store.path, metadataFor(root, projects, headCommit(root), durationMs));
|
|
28
|
+
let validation = 'off';
|
|
29
|
+
if (options.validate) {
|
|
30
|
+
const verification = indexGraph(root, program, projects);
|
|
31
|
+
if (!graphsEqual(candidate, verification))
|
|
32
|
+
throw new Error('Index validation failed: the conservative refresh and full rebuild produced different graphs.');
|
|
33
|
+
validation = 'matched';
|
|
34
|
+
}
|
|
35
|
+
return { built: built(candidate, graph, durationMs), changedFiles, stored, validation };
|
|
36
|
+
}
|
|
37
|
+
export function graphsEqual(left, right) {
|
|
38
|
+
return graphFingerprint(storeGraph(left)) === graphFingerprint(storeGraph(right));
|
|
39
|
+
}
|
|
40
|
+
function indexGraph(root, program, projects) {
|
|
41
|
+
return addHeritage(root, program, projects, walkIndex(root, program, projects));
|
|
42
|
+
}
|
|
43
|
+
function differingFiles(previous, next) {
|
|
44
|
+
const previousHeritage = heritageByFile(previous.graph, previous.heritage);
|
|
45
|
+
const nextHeritage = heritageByFile(next.graph, next.relationships);
|
|
46
|
+
const files = new Set([...previous.graph.files.keys(), ...next.graph.files.keys()]);
|
|
47
|
+
return [...files].filter((file) => fileFingerprint(previous.graph, previousHeritage.get(file) ?? [], file) !== fileFingerprint(next.graph, nextHeritage.get(file) ?? [], file)).sort();
|
|
48
|
+
}
|
|
49
|
+
export function fileFingerprint(graph, heritage, file) {
|
|
50
|
+
const record = graph.files.get(file);
|
|
51
|
+
const declarations = [...new Set(record?.declarations ?? [])].flatMap((key) => {
|
|
52
|
+
const declaration = graph.declarations.get(key);
|
|
53
|
+
return declaration === undefined ? [] : [declaration];
|
|
54
|
+
});
|
|
55
|
+
return `{"declarations":[${serializedRows(declarations).join(',')}],"relationships":[${serializedRows([...(record?.relationships ?? []), ...heritage]).join(',')}]}`;
|
|
56
|
+
}
|
|
57
|
+
function heritageByFile(graph, heritage) {
|
|
58
|
+
const filesByDeclaration = new Map();
|
|
59
|
+
for (const [file, record] of graph.files)
|
|
60
|
+
for (const key of record.declarations)
|
|
61
|
+
(filesByDeclaration.get(key) ?? filesByDeclaration.set(key, []).get(key)).push(file);
|
|
62
|
+
const grouped = new Map();
|
|
63
|
+
for (const relationship of heritage)
|
|
64
|
+
for (const file of filesByDeclaration.get(relationship.from) ?? [])
|
|
65
|
+
(grouped.get(file) ?? grouped.set(file, []).get(file)).push(relationship);
|
|
66
|
+
return grouped;
|
|
67
|
+
}
|
|
68
|
+
function fileGraph(graph, files) {
|
|
69
|
+
const replaced = new Set(files);
|
|
70
|
+
const declarations = graph.declarations.filter((declaration) => replaced.has(declaration.file));
|
|
71
|
+
const keys = new Set(declarations.map((declaration) => declaration.key));
|
|
72
|
+
const relationships = graph.relationships.filter((relationship) => keys.has(relationship.from) || keys.has(relationship.to));
|
|
73
|
+
const externalKeys = new Set(relationships.filter((relationship) => relationship.target === 'External').map((relationship) => relationship.to));
|
|
74
|
+
return { declarations, externals: graph.externals.filter((external) => externalKeys.has(external.key)), relationships };
|
|
75
|
+
}
|
|
76
|
+
function built(index, graph, durationMs) {
|
|
77
|
+
const receipt = receiptFor(graph);
|
|
78
|
+
return {
|
|
79
|
+
graph: index.graph,
|
|
80
|
+
heritage: index.relationships,
|
|
81
|
+
receipt: {
|
|
82
|
+
...receipt,
|
|
83
|
+
internalSites: index.graph.internalSites,
|
|
84
|
+
unresolvedOccurrences: index.graph.unresolvedOccurrences,
|
|
85
|
+
dispatchCalls: index.dispatchCalls,
|
|
86
|
+
durationMs,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function receiptFor(graph) {
|
|
91
|
+
const relationships = Object.fromEntries(['CONTAINS', 'REFERENCES', 'CALLS', 'IMPLEMENTS', 'EXTENDS', 'TYPE_MENTION', 'IMPORTS', 'EXPORTS', 'CHANGED', 'RENAMED_FROM'].map((type) => [type, 0]));
|
|
92
|
+
for (const relationship of graph.relationships)
|
|
93
|
+
relationships[relationship.type]++;
|
|
94
|
+
return { declarations: graph.declarations.length, externals: graph.externals.length, commits: graph.commits?.length ?? 0, relationships };
|
|
95
|
+
}
|
|
96
|
+
export function graphFingerprint(graph) {
|
|
97
|
+
const commits = graph.commits === undefined ? '' : `,"commits":[${serializedRows(graph.commits).join(',')}]`;
|
|
98
|
+
return `{"declarations":[${serializedRows(graph.declarations).join(',')}],"externals":[${serializedRows(graph.externals).join(',')}]${commits},"relationships":[${serializedRows(graph.relationships).join(',')}]}`;
|
|
99
|
+
}
|
|
100
|
+
function serializedRows(rows) {
|
|
101
|
+
return rows.map((row) => JSON.stringify(row)).sort((left, right) => left.localeCompare(right));
|
|
102
|
+
}
|
|
103
|
+
function headCommit(root) {
|
|
104
|
+
try {
|
|
105
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return 'unknown';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type DescriptorScope, type KeyedDeclaration } from './descriptor.js';
|
|
2
|
+
export declare function isIndexedDeclaration(declaration: KeyedDeclaration): boolean;
|
|
3
|
+
export declare function indexedDeclarations(file: string, text: string): KeyedDeclaration[];
|
|
4
|
+
export declare function scopeOf(declaration: KeyedDeclaration): DescriptorScope;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describeFile } from './descriptor.js';
|
|
2
|
+
export function isIndexedDeclaration(declaration) {
|
|
3
|
+
switch (declaration.scope) {
|
|
4
|
+
case 'module':
|
|
5
|
+
case 'top-level':
|
|
6
|
+
case 'named-member':
|
|
7
|
+
case 'function-local':
|
|
8
|
+
return true;
|
|
9
|
+
case 'anonymous-expression':
|
|
10
|
+
return declaration.ownsCallSite;
|
|
11
|
+
case 'excluded':
|
|
12
|
+
case 'other':
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function indexedDeclarations(file, text) {
|
|
17
|
+
return describeFile(file, text).filter(isIndexedDeclaration);
|
|
18
|
+
}
|
|
19
|
+
export function scopeOf(declaration) {
|
|
20
|
+
return declaration.scope;
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { BuiltIndex } from './build.js';
|
|
2
|
+
export type IndexStatus = 'current' | 'indexing' | 'stale';
|
|
3
|
+
export interface IndexSnapshot {
|
|
4
|
+
indexedAt: string;
|
|
5
|
+
state: IndexStatus;
|
|
6
|
+
built: BuiltIndex;
|
|
7
|
+
}
|
|
8
|
+
/** Serializes index rebuilds and exposes only a completed index to store readers. */
|
|
9
|
+
export declare class IndexStateController {
|
|
10
|
+
private readonly publish;
|
|
11
|
+
private snapshot;
|
|
12
|
+
private pending;
|
|
13
|
+
private startStale;
|
|
14
|
+
private staleGeneration;
|
|
15
|
+
constructor(initial: IndexSnapshot, publish: (snapshot: IndexSnapshot) => void);
|
|
16
|
+
current(): IndexSnapshot;
|
|
17
|
+
markStale(start: () => Promise<void>): void;
|
|
18
|
+
rebuild(work: () => Promise<IndexSnapshot>): Promise<void>;
|
|
19
|
+
/** Resolves against a completed index, forcing the watcher's pending batch rather than waiting out its debounce. */
|
|
20
|
+
wait(): Promise<IndexSnapshot>;
|
|
21
|
+
private set;
|
|
22
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Serializes index rebuilds and exposes only a completed index to store readers. */
|
|
2
|
+
export class IndexStateController {
|
|
3
|
+
publish;
|
|
4
|
+
snapshot;
|
|
5
|
+
pending;
|
|
6
|
+
startStale;
|
|
7
|
+
staleGeneration = 0;
|
|
8
|
+
constructor(initial, publish) {
|
|
9
|
+
this.publish = publish;
|
|
10
|
+
this.snapshot = initial;
|
|
11
|
+
publish(initial);
|
|
12
|
+
}
|
|
13
|
+
current() { return this.snapshot; }
|
|
14
|
+
markStale(start) {
|
|
15
|
+
this.staleGeneration++;
|
|
16
|
+
this.startStale = start;
|
|
17
|
+
if (!this.pending)
|
|
18
|
+
this.set({ ...this.snapshot, state: 'stale' });
|
|
19
|
+
}
|
|
20
|
+
rebuild(work) {
|
|
21
|
+
const run = async () => {
|
|
22
|
+
const generation = this.staleGeneration;
|
|
23
|
+
this.set({ ...this.snapshot, state: 'indexing' });
|
|
24
|
+
try {
|
|
25
|
+
const next = await work();
|
|
26
|
+
if (generation !== this.staleGeneration)
|
|
27
|
+
this.set({ ...next, state: 'stale' });
|
|
28
|
+
else {
|
|
29
|
+
this.startStale = undefined;
|
|
30
|
+
this.set(next);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
this.set({ ...this.snapshot, state: 'stale' });
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const previous = this.pending ?? Promise.resolve();
|
|
39
|
+
const tracked = previous.then(run, run).finally(() => { if (this.pending === tracked)
|
|
40
|
+
this.pending = undefined; });
|
|
41
|
+
this.pending = tracked;
|
|
42
|
+
return tracked;
|
|
43
|
+
}
|
|
44
|
+
/** Resolves against a completed index, forcing the watcher's pending batch rather than waiting out its debounce. */
|
|
45
|
+
async wait() {
|
|
46
|
+
for (;;) {
|
|
47
|
+
if (this.snapshot.state === 'stale') {
|
|
48
|
+
const start = this.startStale;
|
|
49
|
+
if (!start)
|
|
50
|
+
throw new Error('The index is stale and no rebuild is scheduled.');
|
|
51
|
+
await start();
|
|
52
|
+
}
|
|
53
|
+
else if (this.pending)
|
|
54
|
+
await this.pending;
|
|
55
|
+
else
|
|
56
|
+
return this.snapshot;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
set(snapshot) {
|
|
60
|
+
this.snapshot = snapshot;
|
|
61
|
+
this.publish(snapshot);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Checker, type Project } from 'typescript/unstable/sync';
|
|
2
|
+
import * as ts from '../ts7/ast.js';
|
|
3
|
+
import { Ts7Host } from '../ts7/api.js';
|
|
4
|
+
import { ConfiguredProjects } from '../ts7/projects.js';
|
|
5
|
+
import { type KeyedDeclaration } from './descriptor.js';
|
|
6
|
+
import type { IndexGraph } from './graph.js';
|
|
7
|
+
export interface WalkFileState {
|
|
8
|
+
absolute: string;
|
|
9
|
+
file: string;
|
|
10
|
+
source: ts.SourceFile;
|
|
11
|
+
project: Project;
|
|
12
|
+
checker: Checker;
|
|
13
|
+
descriptors: readonly KeyedDeclaration[];
|
|
14
|
+
indexed: readonly KeyedDeclaration[];
|
|
15
|
+
byStart: ReadonlyMap<number, KeyedDeclaration>;
|
|
16
|
+
nodesByStart: ReadonlyMap<number, ts.Node>;
|
|
17
|
+
ownersByStart: ReadonlyMap<number, KeyedDeclaration>;
|
|
18
|
+
semanticNodes: readonly ts.Node[];
|
|
19
|
+
typeIdentifierPositions: ReadonlySet<number>;
|
|
20
|
+
}
|
|
21
|
+
/** Returns the snapshot-backed file state created by walkIndex for heritage enrichment. */
|
|
22
|
+
export declare function walkFileStates(graph: IndexGraph): readonly WalkFileState[];
|
|
23
|
+
export declare function walkIndex(root: string, program: Ts7Host, projects: ConfiguredProjects): IndexGraph;
|