@optave/codegraph 3.9.0 → 3.9.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/README.md +7 -6
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +78 -48
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.js +15 -18
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/db/connection.d.ts +1 -0
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +22 -4
- package/dist/db/connection.js.map +1 -1
- package/dist/db/repository/base.d.ts +35 -0
- package/dist/db/repository/base.d.ts.map +1 -1
- package/dist/db/repository/base.js +8 -0
- package/dist/db/repository/base.js.map +1 -1
- package/dist/db/repository/index.d.ts +1 -0
- package/dist/db/repository/index.d.ts.map +1 -1
- package/dist/db/repository/index.js.map +1 -1
- package/dist/db/repository/native-repository.d.ts +7 -1
- package/dist/db/repository/native-repository.d.ts.map +1 -1
- package/dist/db/repository/native-repository.js +46 -1
- package/dist/db/repository/native-repository.js.map +1 -1
- package/dist/domain/analysis/dependencies.d.ts +1 -28
- package/dist/domain/analysis/dependencies.d.ts.map +1 -1
- package/dist/domain/analysis/dependencies.js +12 -0
- package/dist/domain/analysis/dependencies.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +18 -0
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +293 -296
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +29 -2
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js +19 -23
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +99 -95
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +2 -0
- package/dist/domain/parser.js.map +1 -1
- package/dist/extractors/go.js +53 -35
- package/dist/extractors/go.js.map +1 -1
- package/dist/extractors/javascript.js +66 -27
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/features/complexity.d.ts.map +1 -1
- package/dist/features/complexity.js +78 -58
- package/dist/features/complexity.js.map +1 -1
- package/dist/features/dataflow.d.ts.map +1 -1
- package/dist/features/dataflow.js +109 -118
- package/dist/features/dataflow.js.map +1 -1
- package/dist/features/structure.d.ts.map +1 -1
- package/dist/features/structure.js +147 -97
- package/dist/features/structure.js.map +1 -1
- package/dist/graph/algorithms/louvain.d.ts.map +1 -1
- package/dist/graph/algorithms/louvain.js +4 -2
- package/dist/graph/algorithms/louvain.js.map +1 -1
- package/dist/graph/classifiers/roles.d.ts +2 -0
- package/dist/graph/classifiers/roles.d.ts.map +1 -1
- package/dist/graph/classifiers/roles.js +13 -5
- package/dist/graph/classifiers/roles.js.map +1 -1
- package/dist/presentation/communities.d.ts.map +1 -1
- package/dist/presentation/communities.js +38 -34
- package/dist/presentation/communities.js.map +1 -1
- package/dist/presentation/manifesto.d.ts.map +1 -1
- package/dist/presentation/manifesto.js +31 -33
- package/dist/presentation/manifesto.js.map +1 -1
- package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
- package/dist/presentation/queries-cli/inspect.js +47 -46
- package/dist/presentation/queries-cli/inspect.js.map +1 -1
- package/dist/shared/file-utils.d.ts.map +1 -1
- package/dist/shared/file-utils.js +94 -72
- package/dist/shared/file-utils.js.map +1 -1
- package/dist/types.d.ts +81 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/ast-analysis/engine.ts +99 -55
- package/src/ast-analysis/visitors/ast-store-visitor.ts +19 -21
- package/src/db/connection.ts +24 -5
- package/src/db/repository/base.ts +43 -0
- package/src/db/repository/index.ts +1 -0
- package/src/db/repository/native-repository.ts +67 -1
- package/src/domain/analysis/dependencies.ts +13 -0
- package/src/domain/graph/builder/incremental.ts +21 -0
- package/src/domain/graph/builder/pipeline.ts +392 -362
- package/src/domain/graph/builder/stages/build-edges.ts +30 -1
- package/src/domain/graph/builder/stages/resolve-imports.ts +20 -20
- package/src/domain/graph/watcher.ts +118 -98
- package/src/domain/parser.ts +2 -0
- package/src/extractors/go.ts +57 -32
- package/src/extractors/javascript.ts +67 -27
- package/src/features/complexity.ts +94 -58
- package/src/features/dataflow.ts +153 -132
- package/src/features/structure.ts +167 -95
- package/src/graph/algorithms/louvain.ts +5 -2
- package/src/graph/classifiers/roles.ts +14 -5
- package/src/presentation/communities.ts +44 -39
- package/src/presentation/manifesto.ts +35 -38
- package/src/presentation/queries-cli/inspect.ts +48 -46
- package/src/shared/file-utils.ts +116 -77
- package/src/types.ts +85 -0
|
@@ -119,6 +119,23 @@ function buildImportEdges(
|
|
|
119
119
|
: 'imports';
|
|
120
120
|
allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
|
|
121
121
|
|
|
122
|
+
// Type-only imports: create symbol-level edges so the target symbols
|
|
123
|
+
// get fan-in credit and aren't falsely classified as dead code.
|
|
124
|
+
if (imp.typeOnly && ctx.nodesByNameAndFile) {
|
|
125
|
+
for (const name of imp.names) {
|
|
126
|
+
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
127
|
+
let targetFile = resolvedPath;
|
|
128
|
+
if (isBarrelFile(ctx, resolvedPath)) {
|
|
129
|
+
const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
|
|
130
|
+
if (actual) targetFile = actual;
|
|
131
|
+
}
|
|
132
|
+
const candidates = ctx.nodesByNameAndFile.get(`${cleanName}|${targetFile}`);
|
|
133
|
+
if (candidates && candidates.length > 0) {
|
|
134
|
+
allEdgeRows.push([fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0]);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
122
139
|
if (!imp.reexport && isBarrelFile(ctx, resolvedPath)) {
|
|
123
140
|
buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeIdStmt, allEdgeRows);
|
|
124
141
|
}
|
|
@@ -280,7 +297,18 @@ function buildImportEdgesNative(
|
|
|
280
297
|
}
|
|
281
298
|
}
|
|
282
299
|
|
|
283
|
-
// 6.
|
|
300
|
+
// 6. Build symbol node entries for type-only import resolution
|
|
301
|
+
const symbolNodes: Array<{ name: string; file: string; nodeId: number }> = [];
|
|
302
|
+
if (ctx.nodesByNameAndFile) {
|
|
303
|
+
for (const [key, nodes] of ctx.nodesByNameAndFile) {
|
|
304
|
+
if (nodes.length > 0) {
|
|
305
|
+
const [name, file] = key.split('|');
|
|
306
|
+
symbolNodes.push({ name: name!, file: file!, nodeId: nodes[0]!.id });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 7. Call native
|
|
284
312
|
const nativeEdges = native.buildImportEdges!(
|
|
285
313
|
files,
|
|
286
314
|
resolvedImports,
|
|
@@ -288,6 +316,7 @@ function buildImportEdgesNative(
|
|
|
288
316
|
fileNodeIds,
|
|
289
317
|
barrelFiles,
|
|
290
318
|
rootDir,
|
|
319
|
+
symbolNodes,
|
|
291
320
|
) as NativeEdge[];
|
|
292
321
|
|
|
293
322
|
for (const e of nativeEdges) {
|
|
@@ -180,6 +180,13 @@ export function isBarrelFile(ctx: PipelineContext, relPath: string): boolean {
|
|
|
180
180
|
return reexports.length >= ownDefs;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/** Check if a re-export source directly defines the symbol. */
|
|
184
|
+
function sourceDefinesSymbol(ctx: PipelineContext, source: string, symbolName: string): boolean {
|
|
185
|
+
const targetSymbols = ctx.fileSymbols.get(source);
|
|
186
|
+
if (!targetSymbols) return false;
|
|
187
|
+
return targetSymbols.definitions.some((d) => d.name === symbolName);
|
|
188
|
+
}
|
|
189
|
+
|
|
183
190
|
export function resolveBarrelExport(
|
|
184
191
|
ctx: PipelineContext,
|
|
185
192
|
barrelPath: string,
|
|
@@ -188,31 +195,24 @@ export function resolveBarrelExport(
|
|
|
188
195
|
): string | null {
|
|
189
196
|
if (visited.has(barrelPath)) return null;
|
|
190
197
|
visited.add(barrelPath);
|
|
198
|
+
|
|
191
199
|
const reexports = ctx.reexportMap.get(barrelPath) as ReexportEntry[] | undefined;
|
|
192
200
|
if (!reexports) return null;
|
|
201
|
+
|
|
193
202
|
for (const re of reexports) {
|
|
203
|
+
// Named re-export: only follow if the symbol is in the export list
|
|
194
204
|
if (re.names.length > 0 && !re.wildcardReexport) {
|
|
195
|
-
if (re.names.includes(symbolName))
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (hasDef) return re.source;
|
|
200
|
-
const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
|
|
201
|
-
if (deeper) return deeper;
|
|
202
|
-
}
|
|
203
|
-
return re.source;
|
|
204
|
-
}
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
if (re.wildcardReexport || re.names.length === 0) {
|
|
208
|
-
const targetSymbols = ctx.fileSymbols.get(re.source);
|
|
209
|
-
if (targetSymbols) {
|
|
210
|
-
const hasDef = targetSymbols.definitions.some((d) => d.name === symbolName);
|
|
211
|
-
if (hasDef) return re.source;
|
|
212
|
-
const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
|
|
213
|
-
if (deeper) return deeper;
|
|
214
|
-
}
|
|
205
|
+
if (!re.names.includes(symbolName)) continue;
|
|
206
|
+
if (sourceDefinesSymbol(ctx, re.source, symbolName)) return re.source;
|
|
207
|
+
const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
|
|
208
|
+
return deeper ?? re.source;
|
|
215
209
|
}
|
|
210
|
+
|
|
211
|
+
// Wildcard or namespace re-export: check if target defines the symbol
|
|
212
|
+
if (sourceDefinesSymbol(ctx, re.source, symbolName)) return re.source;
|
|
213
|
+
const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
|
|
214
|
+
if (deeper) return deeper;
|
|
216
215
|
}
|
|
216
|
+
|
|
217
217
|
return null;
|
|
218
218
|
}
|
|
@@ -2,20 +2,16 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { closeDb, getNodeId as getNodeIdQuery, initSchema, openDb } from '../../db/index.js';
|
|
4
4
|
import { debug, info } from '../../infrastructure/logger.js';
|
|
5
|
-
import {
|
|
5
|
+
import { isSupportedFile, normalizePath, shouldIgnore } from '../../shared/constants.js';
|
|
6
6
|
import { DbError } from '../../shared/errors.js';
|
|
7
7
|
import { createParseTreeCache, getActiveEngine } from '../parser.js';
|
|
8
8
|
import { type IncrementalStmts, rebuildFile } from './builder/incremental.js';
|
|
9
9
|
import { appendChangeEvents, buildChangeEvent, diffSymbols } from './change-journal.js';
|
|
10
10
|
import { appendJournalEntries } from './journal.js';
|
|
11
11
|
|
|
12
|
-
function
|
|
12
|
+
function shouldIgnorePath(filePath: string): boolean {
|
|
13
13
|
const parts = filePath.split(path.sep);
|
|
14
|
-
return parts.some((p) =>
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function isTrackedExt(filePath: string): boolean {
|
|
18
|
-
return EXTENSIONS.has(path.extname(filePath));
|
|
14
|
+
return parts.some((p) => shouldIgnore(p));
|
|
19
15
|
}
|
|
20
16
|
|
|
21
17
|
/** Prepare all SQL statements needed by the watcher's incremental rebuild. */
|
|
@@ -141,24 +137,35 @@ function collectTrackedFiles(dir: string, result: string[]): void {
|
|
|
141
137
|
let entries: fs.Dirent[];
|
|
142
138
|
try {
|
|
143
139
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
144
|
-
} catch {
|
|
140
|
+
} catch (e: unknown) {
|
|
141
|
+
debug(`collectTrackedFiles: cannot read ${dir}: ${(e as Error).message}`);
|
|
145
142
|
return;
|
|
146
143
|
}
|
|
147
144
|
for (const entry of entries) {
|
|
148
|
-
if (
|
|
145
|
+
if (shouldIgnore(entry.name)) continue;
|
|
149
146
|
const full = path.join(dir, entry.name);
|
|
150
147
|
if (entry.isDirectory()) {
|
|
151
148
|
collectTrackedFiles(full, result);
|
|
152
|
-
} else if (
|
|
149
|
+
} else if (isSupportedFile(entry.name)) {
|
|
153
150
|
result.push(full);
|
|
154
151
|
}
|
|
155
152
|
}
|
|
156
153
|
}
|
|
157
154
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
/** Shared watcher state passed between setup and watcher sub-functions. */
|
|
156
|
+
interface WatcherContext {
|
|
157
|
+
rootDir: string;
|
|
158
|
+
db: ReturnType<typeof openDb>;
|
|
159
|
+
stmts: IncrementalStmts;
|
|
160
|
+
engineOpts: import('../../types.js').EngineOpts;
|
|
161
|
+
cache: ReturnType<typeof createParseTreeCache>;
|
|
162
|
+
pending: Set<string>;
|
|
163
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
164
|
+
debounceMs: number;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Initialize DB, engine, cache, and statements for watch mode. */
|
|
168
|
+
function setupWatcher(rootDir: string, opts: { engine?: string }): WatcherContext {
|
|
162
169
|
const dbPath = path.join(rootDir, '.codegraph', 'graph.db');
|
|
163
170
|
if (!fs.existsSync(dbPath)) {
|
|
164
171
|
throw new DbError('No graph.db found. Run `codegraph build` first.', { file: dbPath });
|
|
@@ -183,111 +190,124 @@ export async function watchProject(
|
|
|
183
190
|
|
|
184
191
|
const stmts = prepareWatcherStatements(db);
|
|
185
192
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
193
|
+
return {
|
|
194
|
+
rootDir,
|
|
195
|
+
db,
|
|
196
|
+
stmts,
|
|
197
|
+
engineOpts,
|
|
198
|
+
cache,
|
|
199
|
+
pending: new Set<string>(),
|
|
200
|
+
timer: null,
|
|
201
|
+
debounceMs: 300,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
192
204
|
|
|
193
|
-
|
|
194
|
-
|
|
205
|
+
/** Schedule debounced processing of pending files. */
|
|
206
|
+
function scheduleDebouncedProcess(ctx: WatcherContext): void {
|
|
207
|
+
if (ctx.timer) clearTimeout(ctx.timer);
|
|
208
|
+
ctx.timer = setTimeout(async () => {
|
|
209
|
+
const files = [...ctx.pending];
|
|
210
|
+
ctx.pending.clear();
|
|
211
|
+
await processPendingFiles(files, ctx.db, ctx.rootDir, ctx.stmts, ctx.engineOpts, ctx.cache);
|
|
212
|
+
}, ctx.debounceMs);
|
|
213
|
+
}
|
|
195
214
|
|
|
196
|
-
|
|
215
|
+
/** Start polling-based file watcher. Returns cleanup function. */
|
|
216
|
+
function startPollingWatcher(ctx: WatcherContext, pollIntervalMs: number): () => void {
|
|
217
|
+
const mtimeMap = new Map<string, number>();
|
|
218
|
+
|
|
219
|
+
const initial: string[] = [];
|
|
220
|
+
collectTrackedFiles(ctx.rootDir, initial);
|
|
221
|
+
for (const f of initial) {
|
|
222
|
+
try {
|
|
223
|
+
mtimeMap.set(f, fs.statSync(f).mtimeMs);
|
|
224
|
+
} catch {
|
|
225
|
+
/* deleted between collect and stat */
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
info(`Polling ${initial.length} tracked files every ${pollIntervalMs}ms`);
|
|
197
229
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
230
|
+
const pollTimer = setInterval(() => {
|
|
231
|
+
const current: string[] = [];
|
|
232
|
+
collectTrackedFiles(ctx.rootDir, current);
|
|
233
|
+
const currentSet = new Set(current);
|
|
202
234
|
|
|
203
|
-
|
|
204
|
-
const initial: string[] = [];
|
|
205
|
-
collectTrackedFiles(rootDir, initial);
|
|
206
|
-
for (const f of initial) {
|
|
235
|
+
for (const f of current) {
|
|
207
236
|
try {
|
|
208
|
-
|
|
237
|
+
const mtime = fs.statSync(f).mtimeMs;
|
|
238
|
+
const prev = mtimeMap.get(f);
|
|
239
|
+
if (prev === undefined || mtime !== prev) {
|
|
240
|
+
mtimeMap.set(f, mtime);
|
|
241
|
+
ctx.pending.add(f);
|
|
242
|
+
}
|
|
209
243
|
} catch {
|
|
210
244
|
/* deleted between collect and stat */
|
|
211
245
|
}
|
|
212
246
|
}
|
|
213
|
-
info(`Polling ${initial.length} tracked files every ${POLL_INTERVAL_MS}ms`);
|
|
214
|
-
|
|
215
|
-
const pollTimer = setInterval(() => {
|
|
216
|
-
const current: string[] = [];
|
|
217
|
-
collectTrackedFiles(rootDir, current);
|
|
218
|
-
const currentSet = new Set(current);
|
|
219
|
-
|
|
220
|
-
// Detect modified or new files
|
|
221
|
-
for (const f of current) {
|
|
222
|
-
try {
|
|
223
|
-
const mtime = fs.statSync(f).mtimeMs;
|
|
224
|
-
const prev = mtimeMap.get(f);
|
|
225
|
-
if (prev === undefined || mtime !== prev) {
|
|
226
|
-
mtimeMap.set(f, mtime);
|
|
227
|
-
pending.add(f);
|
|
228
|
-
}
|
|
229
|
-
} catch {
|
|
230
|
-
/* deleted between collect and stat */
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
247
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
pending.add(f);
|
|
239
|
-
}
|
|
248
|
+
for (const f of mtimeMap.keys()) {
|
|
249
|
+
if (!currentSet.has(f)) {
|
|
250
|
+
mtimeMap.delete(f);
|
|
251
|
+
ctx.pending.add(f);
|
|
240
252
|
}
|
|
253
|
+
}
|
|
241
254
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const watcher = fs.watch(rootDir, { recursive: true }, (_eventType, filename) => {
|
|
257
|
-
if (!filename) return;
|
|
258
|
-
if (shouldIgnore(filename)) return;
|
|
259
|
-
if (!isTrackedExt(filename)) return;
|
|
260
|
-
|
|
261
|
-
const fullPath = path.join(rootDir, filename);
|
|
262
|
-
pending.add(fullPath);
|
|
263
|
-
|
|
264
|
-
if (timer) clearTimeout(timer);
|
|
265
|
-
timer = setTimeout(async () => {
|
|
266
|
-
const files = [...pending];
|
|
267
|
-
pending.clear();
|
|
268
|
-
await processPendingFiles(files, db, rootDir, stmts, engineOpts, cache);
|
|
269
|
-
}, DEBOUNCE_MS);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
cleanup = () => watcher.close();
|
|
273
|
-
}
|
|
255
|
+
if (ctx.pending.size > 0) {
|
|
256
|
+
scheduleDebouncedProcess(ctx);
|
|
257
|
+
}
|
|
258
|
+
}, pollIntervalMs);
|
|
259
|
+
|
|
260
|
+
return () => clearInterval(pollTimer);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Start native OS file watcher. Returns cleanup function. */
|
|
264
|
+
function startNativeWatcher(ctx: WatcherContext): () => void {
|
|
265
|
+
const watcher = fs.watch(ctx.rootDir, { recursive: true }, (_eventType, filename) => {
|
|
266
|
+
if (!filename) return;
|
|
267
|
+
if (shouldIgnorePath(filename)) return;
|
|
268
|
+
if (!isSupportedFile(filename)) return;
|
|
274
269
|
|
|
275
|
-
|
|
270
|
+
ctx.pending.add(path.join(ctx.rootDir, filename));
|
|
271
|
+
scheduleDebouncedProcess(ctx);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return () => watcher.close();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Register SIGINT handler to flush journal and clean up. */
|
|
278
|
+
function setupShutdownHandler(ctx: WatcherContext, cleanup: () => void): void {
|
|
279
|
+
process.once('SIGINT', () => {
|
|
276
280
|
info('Stopping watcher...');
|
|
277
281
|
cleanup();
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
file: normalizePath(path.relative(rootDir, filePath)),
|
|
282
|
+
if (ctx.pending.size > 0) {
|
|
283
|
+
const entries = [...ctx.pending].map((filePath) => ({
|
|
284
|
+
file: normalizePath(path.relative(ctx.rootDir, filePath)),
|
|
282
285
|
}));
|
|
283
286
|
try {
|
|
284
|
-
appendJournalEntries(rootDir, entries);
|
|
287
|
+
appendJournalEntries(ctx.rootDir, entries);
|
|
285
288
|
} catch (e: unknown) {
|
|
286
289
|
debug(`Journal flush on exit failed (non-fatal): ${(e as Error).message}`);
|
|
287
290
|
}
|
|
288
291
|
}
|
|
289
|
-
if (cache) cache.clear();
|
|
290
|
-
closeDb(db);
|
|
292
|
+
if (ctx.cache) ctx.cache.clear();
|
|
293
|
+
closeDb(ctx.db);
|
|
291
294
|
process.exit(0);
|
|
292
295
|
});
|
|
293
296
|
}
|
|
297
|
+
|
|
298
|
+
export async function watchProject(
|
|
299
|
+
rootDir: string,
|
|
300
|
+
opts: { engine?: string; poll?: boolean; pollInterval?: number } = {},
|
|
301
|
+
): Promise<void> {
|
|
302
|
+
const ctx = setupWatcher(rootDir, opts);
|
|
303
|
+
|
|
304
|
+
const usePoll = opts.poll ?? process.platform === 'win32';
|
|
305
|
+
const pollIntervalMs = opts.pollInterval ?? 2000;
|
|
306
|
+
|
|
307
|
+
info(`Watching ${rootDir} for changes${usePoll ? ' (polling mode)' : ''}...`);
|
|
308
|
+
info('Press Ctrl+C to stop.');
|
|
309
|
+
|
|
310
|
+
const cleanup = usePoll ? startPollingWatcher(ctx, pollIntervalMs) : startNativeWatcher(ctx);
|
|
311
|
+
|
|
312
|
+
setupShutdownHandler(ctx, cleanup);
|
|
313
|
+
}
|
package/src/domain/parser.ts
CHANGED
|
@@ -143,6 +143,8 @@ const COMMON_QUERY_PATTERNS: string[] = [
|
|
|
143
143
|
'(call_expression function: (identifier) @callfn_name) @callfn_node',
|
|
144
144
|
'(call_expression function: (member_expression) @callmem_fn) @callmem_node',
|
|
145
145
|
'(call_expression function: (subscript_expression) @callsub_fn) @callsub_node',
|
|
146
|
+
'(new_expression constructor: (identifier) @newfn_name) @newfn_node',
|
|
147
|
+
'(new_expression constructor: (member_expression) @newmem_fn) @newmem_node',
|
|
146
148
|
'(expression_statement (assignment_expression left: (member_expression) @assign_left right: (_) @assign_right)) @assign_node',
|
|
147
149
|
];
|
|
148
150
|
|
package/src/extractors/go.ts
CHANGED
|
@@ -266,44 +266,69 @@ function handleTypedIdentifiers(
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
/** Infer type from a single RHS expression in a short var declaration. */
|
|
269
|
-
|
|
269
|
+
/** x := Struct{...} — composite literal (confidence 1.0). */
|
|
270
|
+
function inferCompositeLiteral(
|
|
270
271
|
varNode: TreeSitterNode,
|
|
271
272
|
rhs: TreeSitterNode,
|
|
272
273
|
typeMap: Map<string, { type: string; confidence: number }>,
|
|
273
|
-
):
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
274
|
+
): boolean {
|
|
275
|
+
if (rhs.type !== 'composite_literal') return false;
|
|
276
|
+
const typeNode = rhs.childForFieldName('type');
|
|
277
|
+
if (!typeNode) return false;
|
|
278
|
+
const typeName = extractGoTypeName(typeNode);
|
|
279
|
+
if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 1.0);
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** x := &Struct{...} — address-of composite literal (confidence 1.0). */
|
|
284
|
+
function inferAddressOfComposite(
|
|
285
|
+
varNode: TreeSitterNode,
|
|
286
|
+
rhs: TreeSitterNode,
|
|
287
|
+
typeMap: Map<string, { type: string; confidence: number }>,
|
|
288
|
+
): boolean {
|
|
289
|
+
if (rhs.type !== 'unary_expression') return false;
|
|
290
|
+
const operand = rhs.childForFieldName('operand');
|
|
291
|
+
if (!operand || operand.type !== 'composite_literal') return false;
|
|
292
|
+
const typeNode = operand.childForFieldName('type');
|
|
293
|
+
if (!typeNode) return false;
|
|
294
|
+
const typeName = extractGoTypeName(typeNode);
|
|
295
|
+
if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 1.0);
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** x := NewFoo() or x := pkg.NewFoo() — factory function (confidence 0.7). */
|
|
300
|
+
function inferFactoryCall(
|
|
301
|
+
varNode: TreeSitterNode,
|
|
302
|
+
rhs: TreeSitterNode,
|
|
303
|
+
typeMap: Map<string, { type: string; confidence: number }>,
|
|
304
|
+
): boolean {
|
|
305
|
+
if (rhs.type !== 'call_expression') return false;
|
|
306
|
+
const fn = rhs.childForFieldName('function');
|
|
307
|
+
if (!fn) return false;
|
|
308
|
+
|
|
309
|
+
if (fn.type === 'selector_expression') {
|
|
310
|
+
const field = fn.childForFieldName('field');
|
|
311
|
+
if (field?.text.startsWith('New')) {
|
|
312
|
+
const typeName = field.text.slice(3);
|
|
304
313
|
if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 0.7);
|
|
314
|
+
return true;
|
|
305
315
|
}
|
|
316
|
+
} else if (fn.type === 'identifier' && fn.text.startsWith('New')) {
|
|
317
|
+
const typeName = fn.text.slice(3);
|
|
318
|
+
if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 0.7);
|
|
319
|
+
return true;
|
|
306
320
|
}
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function inferShortVarType(
|
|
325
|
+
varNode: TreeSitterNode,
|
|
326
|
+
rhs: TreeSitterNode,
|
|
327
|
+
typeMap: Map<string, { type: string; confidence: number }>,
|
|
328
|
+
): void {
|
|
329
|
+
if (inferCompositeLiteral(varNode, rhs, typeMap)) return;
|
|
330
|
+
if (inferAddressOfComposite(varNode, rhs, typeMap)) return;
|
|
331
|
+
inferFactoryCall(varNode, rhs, typeMap);
|
|
307
332
|
}
|
|
308
333
|
|
|
309
334
|
/** Handle short_var_declaration: x := Struct{}, x := &Struct{}, x := NewFoo(). */
|
|
@@ -202,6 +202,48 @@ function handleExportCapture(
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
function handleInterfaceCapture(
|
|
206
|
+
c: Record<string, TreeSitterNode>,
|
|
207
|
+
definitions: Definition[],
|
|
208
|
+
): void {
|
|
209
|
+
const ifaceNode = c.iface_node!;
|
|
210
|
+
const ifaceName = c.iface_name!.text;
|
|
211
|
+
definitions.push({
|
|
212
|
+
name: ifaceName,
|
|
213
|
+
kind: 'interface',
|
|
214
|
+
line: ifaceNode.startPosition.row + 1,
|
|
215
|
+
endLine: nodeEndLine(ifaceNode),
|
|
216
|
+
});
|
|
217
|
+
const body =
|
|
218
|
+
ifaceNode.childForFieldName('body') ||
|
|
219
|
+
findChild(ifaceNode, 'interface_body') ||
|
|
220
|
+
findChild(ifaceNode, 'object_type');
|
|
221
|
+
if (body) extractInterfaceMethods(body, ifaceName, definitions);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function handleTypeCapture(c: Record<string, TreeSitterNode>, definitions: Definition[]): void {
|
|
225
|
+
const typeNode = c.type_node!;
|
|
226
|
+
definitions.push({
|
|
227
|
+
name: c.type_name!.text,
|
|
228
|
+
kind: 'type',
|
|
229
|
+
line: typeNode.startPosition.row + 1,
|
|
230
|
+
endLine: nodeEndLine(typeNode),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function handleImportCapture(c: Record<string, TreeSitterNode>, imports: Import[]): void {
|
|
235
|
+
const impNode = c.imp_node!;
|
|
236
|
+
const isTypeOnly = impNode.text.startsWith('import type');
|
|
237
|
+
const modPath = c.imp_source!.text.replace(/['"]/g, '');
|
|
238
|
+
const names = extractImportNames(impNode);
|
|
239
|
+
imports.push({
|
|
240
|
+
source: modPath,
|
|
241
|
+
names,
|
|
242
|
+
line: impNode.startPosition.row + 1,
|
|
243
|
+
typeOnly: isTypeOnly,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
205
247
|
/** Dispatch a single query match to the appropriate handler. */
|
|
206
248
|
function dispatchQueryMatch(
|
|
207
249
|
c: Record<string, TreeSitterNode>,
|
|
@@ -220,35 +262,11 @@ function dispatchQueryMatch(
|
|
|
220
262
|
} else if (c.meth_node) {
|
|
221
263
|
handleMethodCapture(c, definitions);
|
|
222
264
|
} else if (c.iface_node) {
|
|
223
|
-
|
|
224
|
-
definitions.push({
|
|
225
|
-
name: ifaceName,
|
|
226
|
-
kind: 'interface',
|
|
227
|
-
line: c.iface_node.startPosition.row + 1,
|
|
228
|
-
endLine: nodeEndLine(c.iface_node),
|
|
229
|
-
});
|
|
230
|
-
const body =
|
|
231
|
-
c.iface_node.childForFieldName('body') ||
|
|
232
|
-
findChild(c.iface_node, 'interface_body') ||
|
|
233
|
-
findChild(c.iface_node, 'object_type');
|
|
234
|
-
if (body) extractInterfaceMethods(body, ifaceName, definitions);
|
|
265
|
+
handleInterfaceCapture(c, definitions);
|
|
235
266
|
} else if (c.type_node) {
|
|
236
|
-
definitions
|
|
237
|
-
name: c.type_name!.text,
|
|
238
|
-
kind: 'type',
|
|
239
|
-
line: c.type_node.startPosition.row + 1,
|
|
240
|
-
endLine: nodeEndLine(c.type_node),
|
|
241
|
-
});
|
|
267
|
+
handleTypeCapture(c, definitions);
|
|
242
268
|
} else if (c.imp_node) {
|
|
243
|
-
|
|
244
|
-
const modPath = c.imp_source!.text.replace(/['"]/g, '');
|
|
245
|
-
const names = extractImportNames(c.imp_node);
|
|
246
|
-
imports.push({
|
|
247
|
-
source: modPath,
|
|
248
|
-
names,
|
|
249
|
-
line: c.imp_node.startPosition.row + 1,
|
|
250
|
-
typeOnly: isTypeOnly,
|
|
251
|
-
});
|
|
269
|
+
handleImportCapture(c, imports);
|
|
252
270
|
} else if (c.exp_node) {
|
|
253
271
|
handleExportCapture(c, exps, imports);
|
|
254
272
|
} else if (c.callfn_node) {
|
|
@@ -264,6 +282,14 @@ function dispatchQueryMatch(
|
|
|
264
282
|
} else if (c.callsub_node) {
|
|
265
283
|
const callInfo = extractCallInfo(c.callsub_fn!, c.callsub_node);
|
|
266
284
|
if (callInfo) calls.push(callInfo);
|
|
285
|
+
} else if (c.newfn_node) {
|
|
286
|
+
calls.push({
|
|
287
|
+
name: c.newfn_name!.text,
|
|
288
|
+
line: c.newfn_node.startPosition.row + 1,
|
|
289
|
+
});
|
|
290
|
+
} else if (c.newmem_node) {
|
|
291
|
+
const callInfo = extractCallInfo(c.newmem_fn!, c.newmem_node);
|
|
292
|
+
if (callInfo) calls.push(callInfo);
|
|
267
293
|
} else if (c.assign_node) {
|
|
268
294
|
handleCommonJSAssignment(c.assign_left!, c.assign_right!, c.assign_node, imports);
|
|
269
295
|
}
|
|
@@ -502,6 +528,9 @@ function walkJavaScriptNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
|
502
528
|
case 'call_expression':
|
|
503
529
|
handleCallExpr(node, ctx);
|
|
504
530
|
break;
|
|
531
|
+
case 'new_expression':
|
|
532
|
+
handleNewExpr(node, ctx);
|
|
533
|
+
break;
|
|
505
534
|
case 'import_statement':
|
|
506
535
|
handleImportStmt(node, ctx);
|
|
507
536
|
break;
|
|
@@ -689,6 +718,17 @@ function handleCallExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
|
689
718
|
}
|
|
690
719
|
}
|
|
691
720
|
|
|
721
|
+
function handleNewExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
722
|
+
const ctor = node.childForFieldName('constructor') || node.child(1);
|
|
723
|
+
if (!ctor) return;
|
|
724
|
+
if (ctor.type === 'identifier') {
|
|
725
|
+
ctx.calls.push({ name: ctor.text, line: node.startPosition.row + 1 });
|
|
726
|
+
} else if (ctor.type === 'member_expression') {
|
|
727
|
+
const callInfo = extractCallInfo(ctor, node);
|
|
728
|
+
if (callInfo) ctx.calls.push(callInfo);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
692
732
|
/** Handle a dynamic import() call expression and add to imports if static. */
|
|
693
733
|
function handleDynamicImportCall(node: TreeSitterNode, imports: Import[]): void {
|
|
694
734
|
const args = node.childForFieldName('arguments') || findChild(node, 'arguments');
|