agentsys 5.6.4 → 5.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +30 -19
- package/.claude-plugin/plugin.json +1 -1
- package/.kiro/agents/exploration-agent.json +1 -1
- package/.kiro/agents/implementation-agent.json +1 -1
- package/.kiro/agents/map-validator.json +2 -2
- package/.kiro/agents/perf-orchestrator.json +1 -1
- package/.kiro/agents/planning-agent.json +1 -1
- package/.kiro/skills/perf-code-paths/SKILL.md +1 -1
- package/.kiro/skills/perf-theory-gatherer/SKILL.md +1 -1
- package/.kiro/skills/repo-intel/SKILL.md +63 -0
- package/AGENTS.md +10 -8
- package/CHANGELOG.md +37 -0
- package/README.md +152 -98
- package/lib/binary/version.js +1 -1
- package/lib/repo-map/converter.js +130 -0
- package/lib/repo-map/index.js +117 -74
- package/lib/repo-map/installer.js +38 -172
- package/lib/repo-map/updater.js +16 -474
- package/meta/skills/maintain-cross-platform/SKILL.md +7 -6
- package/package.json +3 -3
- package/scripts/fix-graduated-repos.js +2 -2
- package/scripts/generate-docs.js +22 -16
- package/scripts/graduate-plugin.js +1 -1
- package/scripts/plugins.txt +7 -1
- package/scripts/preflight.js +4 -4
- package/scripts/validate-cross-platform-docs.js +2 -2
- package/site/content.json +40 -23
- package/site/index.html +44 -12
- package/site/ux-spec.md +6 -6
- package/.kiro/skills/repo-mapping/SKILL.md +0 -83
- package/lib/repo-map/concurrency.js +0 -29
- package/lib/repo-map/queries/go.js +0 -27
- package/lib/repo-map/queries/index.js +0 -100
- package/lib/repo-map/queries/java.js +0 -38
- package/lib/repo-map/queries/javascript.js +0 -55
- package/lib/repo-map/queries/python.js +0 -24
- package/lib/repo-map/queries/rust.js +0 -73
- package/lib/repo-map/queries/typescript.js +0 -38
- package/lib/repo-map/runner.js +0 -1364
- package/lib/repo-map/usage-analyzer.js +0 -407
package/lib/repo-map/runner.js
DELETED
|
@@ -1,1364 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ast-grep execution and result parsing
|
|
3
|
-
*
|
|
4
|
-
* @module lib/repo-map/runner
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
'use strict';
|
|
8
|
-
|
|
9
|
-
const { execFileSync, spawnSync, spawn } = require('child_process');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const fsPromises = require('fs').promises;
|
|
13
|
-
const crypto = require('crypto');
|
|
14
|
-
|
|
15
|
-
const installer = require('./installer');
|
|
16
|
-
const queries = require('./queries');
|
|
17
|
-
const slopAnalyzers = require('../patterns/slop-analyzers');
|
|
18
|
-
const { runWithConcurrency } = require('./concurrency');
|
|
19
|
-
|
|
20
|
-
// Language file extensions mapping
|
|
21
|
-
const LANGUAGE_EXTENSIONS = {
|
|
22
|
-
javascript: ['.js', '.jsx', '.mjs', '.cjs'],
|
|
23
|
-
typescript: ['.ts', '.tsx', '.mts', '.cts'],
|
|
24
|
-
python: ['.py', '.pyw'],
|
|
25
|
-
rust: ['.rs'],
|
|
26
|
-
go: ['.go'],
|
|
27
|
-
java: ['.java']
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// Directories to exclude from scanning (extend base list)
|
|
31
|
-
const EXCLUDE_DIRS = Array.from(new Set([
|
|
32
|
-
...slopAnalyzers.EXCLUDE_DIRS,
|
|
33
|
-
'.claude', '.opencode', '.codex', '.venv', 'venv', 'env'
|
|
34
|
-
]));
|
|
35
|
-
|
|
36
|
-
const AST_GREP_BATCH_SIZE = 100;
|
|
37
|
-
const AST_GREP_CONCURRENCY = 4;
|
|
38
|
-
const LANGUAGE_EXTENSION_SCAN_LIMIT = 500;
|
|
39
|
-
const FILE_READ_BATCH_SIZE = 50; // Concurrent file reads
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Detect languages in a repository
|
|
43
|
-
* @param {string} basePath - Repository root
|
|
44
|
-
* @returns {Promise<string[]>} - List of detected languages
|
|
45
|
-
*/
|
|
46
|
-
async function detectLanguages(basePath) {
|
|
47
|
-
const detected = new Set();
|
|
48
|
-
|
|
49
|
-
// Check for config files first (faster)
|
|
50
|
-
const configIndicators = {
|
|
51
|
-
javascript: ['package.json', 'jsconfig.json'],
|
|
52
|
-
typescript: ['tsconfig.json', 'tsconfig.base.json'],
|
|
53
|
-
python: ['pyproject.toml', 'setup.py', 'requirements.txt', 'Pipfile'],
|
|
54
|
-
rust: ['Cargo.toml'],
|
|
55
|
-
go: ['go.mod', 'go.sum'],
|
|
56
|
-
java: ['pom.xml', 'build.gradle', 'build.gradle.kts']
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
for (const [lang, files] of Object.entries(configIndicators)) {
|
|
60
|
-
for (const file of files) {
|
|
61
|
-
if (fs.existsSync(path.join(basePath, file))) {
|
|
62
|
-
detected.add(lang);
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Supplement with extension scan to catch mixed-language repos
|
|
69
|
-
const extensions = scanForExtensions(basePath, LANGUAGE_EXTENSION_SCAN_LIMIT);
|
|
70
|
-
for (const [lang, exts] of Object.entries(LANGUAGE_EXTENSIONS)) {
|
|
71
|
-
if (exts.some(ext => extensions.has(ext))) {
|
|
72
|
-
detected.add(lang);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return Array.from(detected);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Scan repository for file extensions (sampling)
|
|
81
|
-
* @param {string} basePath - Repository root
|
|
82
|
-
* @param {number} maxFiles - Maximum files to check
|
|
83
|
-
* @returns {Set<string>} - Set of extensions found
|
|
84
|
-
*/
|
|
85
|
-
function scanForExtensions(basePath, maxFiles = 100) {
|
|
86
|
-
const extensions = new Set();
|
|
87
|
-
let count = 0;
|
|
88
|
-
const isIgnored = slopAnalyzers.parseGitignore(basePath, fs, path);
|
|
89
|
-
|
|
90
|
-
function scan(dir) {
|
|
91
|
-
if (count >= maxFiles) return;
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
95
|
-
for (const entry of entries) {
|
|
96
|
-
if (count >= maxFiles) break;
|
|
97
|
-
|
|
98
|
-
if (entry.isDirectory()) {
|
|
99
|
-
const relativePath = path.relative(basePath, path.join(dir, entry.name));
|
|
100
|
-
if (slopAnalyzers.shouldExclude(relativePath, EXCLUDE_DIRS)) continue;
|
|
101
|
-
if (isIgnored && isIgnored(relativePath, true)) continue;
|
|
102
|
-
if (!entry.name.startsWith('.')) {
|
|
103
|
-
scan(path.join(dir, entry.name));
|
|
104
|
-
}
|
|
105
|
-
} else if (entry.isFile()) {
|
|
106
|
-
const relativePath = path.relative(basePath, path.join(dir, entry.name));
|
|
107
|
-
if (slopAnalyzers.shouldExclude(relativePath, EXCLUDE_DIRS)) continue;
|
|
108
|
-
if (isIgnored && isIgnored(relativePath, false)) continue;
|
|
109
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
110
|
-
if (ext) {
|
|
111
|
-
extensions.add(ext);
|
|
112
|
-
count++;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
} catch {
|
|
117
|
-
// Skip directories we can't read
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
scan(basePath);
|
|
122
|
-
return extensions;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Run a full scan of the repository
|
|
127
|
-
* @param {string} basePath - Repository root
|
|
128
|
-
* @param {string[]} languages - Languages to scan
|
|
129
|
-
* @returns {Promise<Object>} - The generated map
|
|
130
|
-
*/
|
|
131
|
-
async function fullScan(basePath, languages, options = {}) {
|
|
132
|
-
const cmd = installer.getCommand();
|
|
133
|
-
if (!cmd) {
|
|
134
|
-
throw new Error('ast-grep not found');
|
|
135
|
-
}
|
|
136
|
-
const fileLimit = Number.isFinite(options.fileLimit) ? Math.max(0, Math.floor(options.fileLimit)) : null;
|
|
137
|
-
const filesByLanguage = collectFilesByLanguage(basePath, languages, {
|
|
138
|
-
maxFiles: fileLimit
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const map = {
|
|
142
|
-
version: '1.0.0',
|
|
143
|
-
generated: new Date().toISOString(),
|
|
144
|
-
updated: null,
|
|
145
|
-
git: getGitInfo(basePath),
|
|
146
|
-
project: {
|
|
147
|
-
type: detectProjectType(languages),
|
|
148
|
-
languages,
|
|
149
|
-
frameworks: [] // Could be enhanced later
|
|
150
|
-
},
|
|
151
|
-
stats: {
|
|
152
|
-
totalFiles: 0,
|
|
153
|
-
totalSymbols: 0,
|
|
154
|
-
scanDurationMs: 0,
|
|
155
|
-
errors: []
|
|
156
|
-
},
|
|
157
|
-
files: {},
|
|
158
|
-
dependencies: {}
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
// Run queries for each language
|
|
162
|
-
for (const lang of languages) {
|
|
163
|
-
const langQueries = queries.getQueriesForLanguage(lang);
|
|
164
|
-
if (!langQueries) continue;
|
|
165
|
-
|
|
166
|
-
const files = filesByLanguage.get(lang) || [];
|
|
167
|
-
if (files.length === 0) continue;
|
|
168
|
-
|
|
169
|
-
const fileEntries = [];
|
|
170
|
-
const symbolMapsByFile = new Map();
|
|
171
|
-
const importStateByFile = new Map();
|
|
172
|
-
const contentByFile = new Map();
|
|
173
|
-
|
|
174
|
-
// Filter out already processed files first
|
|
175
|
-
const filesToProcess = files.filter(file => {
|
|
176
|
-
const relativePath = path.relative(basePath, file).replace(/\\/g, '/');
|
|
177
|
-
return !map.files[relativePath];
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// Batch read all files asynchronously
|
|
181
|
-
const fileContents = await batchReadFiles(filesToProcess);
|
|
182
|
-
|
|
183
|
-
for (const file of filesToProcess) {
|
|
184
|
-
const relativePath = path.relative(basePath, file).replace(/\\/g, '/');
|
|
185
|
-
const readResult = fileContents.get(file);
|
|
186
|
-
|
|
187
|
-
if (readResult.error || readResult.content === null) {
|
|
188
|
-
map.stats.errors.push({
|
|
189
|
-
file: relativePath,
|
|
190
|
-
error: readResult.error?.message || 'Failed to read file'
|
|
191
|
-
});
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const content = readResult.content;
|
|
196
|
-
const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
197
|
-
|
|
198
|
-
map.files[relativePath] = {
|
|
199
|
-
hash,
|
|
200
|
-
language: lang,
|
|
201
|
-
size: content.length,
|
|
202
|
-
symbols: {
|
|
203
|
-
exports: [],
|
|
204
|
-
functions: [],
|
|
205
|
-
classes: [],
|
|
206
|
-
types: [],
|
|
207
|
-
constants: []
|
|
208
|
-
},
|
|
209
|
-
imports: []
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
map.stats.totalFiles++;
|
|
213
|
-
fileEntries.push({ file, relativePath });
|
|
214
|
-
symbolMapsByFile.set(relativePath, createSymbolMaps());
|
|
215
|
-
importStateByFile.set(relativePath, { items: [], seen: new Set() });
|
|
216
|
-
contentByFile.set(relativePath, content);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (fileEntries.length === 0) continue;
|
|
220
|
-
|
|
221
|
-
const filesBySgLang = new Map();
|
|
222
|
-
for (const entry of fileEntries) {
|
|
223
|
-
const sgLang = queries.getSgLanguageForFile(entry.file, lang);
|
|
224
|
-
if (!filesBySgLang.has(sgLang)) {
|
|
225
|
-
filesBySgLang.set(sgLang, []);
|
|
226
|
-
}
|
|
227
|
-
filesBySgLang.get(sgLang).push(entry);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
for (const [sgLang, entries] of filesBySgLang) {
|
|
231
|
-
const filePaths = entries.map(entry => entry.file);
|
|
232
|
-
const experimentBatchSize = process.env.PERF_EXPERIMENT === '1'
|
|
233
|
-
? Number(process.env.REPO_MAP_AST_GREP_BATCH_SIZE)
|
|
234
|
-
: null;
|
|
235
|
-
const batchSize = Number.isFinite(options.astGrepBatchSize)
|
|
236
|
-
? Math.max(1, Math.floor(options.astGrepBatchSize))
|
|
237
|
-
: Number.isFinite(experimentBatchSize) && experimentBatchSize > 0
|
|
238
|
-
? Math.max(1, Math.floor(experimentBatchSize))
|
|
239
|
-
: AST_GREP_BATCH_SIZE;
|
|
240
|
-
const chunks = chunkArray(filePaths, batchSize);
|
|
241
|
-
|
|
242
|
-
const patternGroups = [
|
|
243
|
-
{ category: 'exports', patterns: langQueries.exports, defaultKind: 'export' },
|
|
244
|
-
{ category: 'functions', patterns: langQueries.functions, defaultKind: 'function' },
|
|
245
|
-
{ category: 'classes', patterns: langQueries.classes, defaultKind: 'class' },
|
|
246
|
-
{ category: 'types', patterns: langQueries.types, defaultKind: 'type' },
|
|
247
|
-
{ category: 'constants', patterns: langQueries.constants, defaultKind: 'constant' },
|
|
248
|
-
{ category: 'imports', patterns: langQueries.imports, defaultKind: 'import' }
|
|
249
|
-
];
|
|
250
|
-
|
|
251
|
-
for (const group of patternGroups) {
|
|
252
|
-
if (!group.patterns || group.patterns.length === 0) continue;
|
|
253
|
-
|
|
254
|
-
for (const patternDef of group.patterns) {
|
|
255
|
-
const pattern = typeof patternDef === 'string' ? patternDef : patternDef.pattern;
|
|
256
|
-
if (!pattern) continue;
|
|
257
|
-
|
|
258
|
-
const matchesByChunk = await runAstGrepPatternBatches(cmd, pattern, sgLang, basePath, chunks, {
|
|
259
|
-
onError: (error) => map.stats.errors.push(error),
|
|
260
|
-
concurrency: options.astGrepConcurrency
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
for (const matches of matchesByChunk) {
|
|
264
|
-
for (const match of matches) {
|
|
265
|
-
const matchedPath = normalizeMatchPath(match.file, basePath);
|
|
266
|
-
if (!matchedPath) continue;
|
|
267
|
-
|
|
268
|
-
const symbolMaps = symbolMapsByFile.get(matchedPath);
|
|
269
|
-
const importState = importStateByFile.get(matchedPath);
|
|
270
|
-
if (!symbolMaps || !importState) continue;
|
|
271
|
-
|
|
272
|
-
if (group.category === 'imports') {
|
|
273
|
-
const sourceResult = extractSourceFromMatch(match, patternDef);
|
|
274
|
-
const sources = Array.isArray(sourceResult) ? sourceResult : [sourceResult];
|
|
275
|
-
for (const source of sources) {
|
|
276
|
-
if (!source) continue;
|
|
277
|
-
const kind = patternDef.kind || 'import';
|
|
278
|
-
const key = `${source}:${kind}`;
|
|
279
|
-
if (importState.seen.has(key)) continue;
|
|
280
|
-
importState.seen.add(key);
|
|
281
|
-
importState.items.push({
|
|
282
|
-
source,
|
|
283
|
-
kind,
|
|
284
|
-
line: getLine(match)
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const names = extractNamesFromMatch(match, patternDef);
|
|
291
|
-
const targetMap = symbolMaps[group.category];
|
|
292
|
-
if (!targetMap) continue;
|
|
293
|
-
for (const name of names) {
|
|
294
|
-
const kind = patternDef.kind || group.defaultKind;
|
|
295
|
-
addSymbolToMap(targetMap, name, match, kind, patternDef.extra);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
for (const entry of fileEntries) {
|
|
304
|
-
const relativePath = entry.relativePath;
|
|
305
|
-
const symbolMaps = symbolMapsByFile.get(relativePath);
|
|
306
|
-
const importState = importStateByFile.get(relativePath);
|
|
307
|
-
if (!symbolMaps || !importState) continue;
|
|
308
|
-
|
|
309
|
-
const exportNames = new Set(symbolMaps.exports.keys());
|
|
310
|
-
const content = contentByFile.get(relativePath) || '';
|
|
311
|
-
applyLanguageExportRules(lang, content, exportNames, symbolMaps.functions, symbolMaps.classes, symbolMaps.types, symbolMaps.constants);
|
|
312
|
-
ensureExportEntries(symbolMaps.exports, exportNames, symbolMaps.functions, symbolMaps.classes, symbolMaps.types, symbolMaps.constants);
|
|
313
|
-
|
|
314
|
-
const symbols = {
|
|
315
|
-
exports: mapToSortedArray(symbolMaps.exports),
|
|
316
|
-
functions: mapToSortedArray(symbolMaps.functions, exportNames),
|
|
317
|
-
classes: mapToSortedArray(symbolMaps.classes, exportNames),
|
|
318
|
-
types: mapToSortedArray(symbolMaps.types, exportNames),
|
|
319
|
-
constants: mapToSortedArray(symbolMaps.constants, exportNames)
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
map.files[relativePath].symbols = symbols;
|
|
323
|
-
map.files[relativePath].imports = importState.items;
|
|
324
|
-
|
|
325
|
-
if (importState.items.length > 0) {
|
|
326
|
-
map.dependencies[relativePath] = Array.from(new Set(importState.items.map(imp => imp.source)));
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
map.stats.totalSymbols +=
|
|
330
|
-
(symbols.functions?.length || 0) +
|
|
331
|
-
(symbols.classes?.length || 0) +
|
|
332
|
-
(symbols.types?.length || 0) +
|
|
333
|
-
(symbols.constants?.length || 0);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return map;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Find all files for a language
|
|
342
|
-
* @param {string} basePath - Repository root
|
|
343
|
-
* @param {string} language - Language name
|
|
344
|
-
* @returns {string[]} - Array of file paths
|
|
345
|
-
*/
|
|
346
|
-
function findFilesForLanguage(basePath, language, options = {}) {
|
|
347
|
-
const extensions = LANGUAGE_EXTENSIONS[language] || [];
|
|
348
|
-
const files = [];
|
|
349
|
-
const isIgnored = slopAnalyzers.parseGitignore(basePath, fs, path);
|
|
350
|
-
const maxFiles = Number.isFinite(options.maxFiles) ? Math.max(0, Math.floor(options.maxFiles)) : null;
|
|
351
|
-
|
|
352
|
-
function scan(dir) {
|
|
353
|
-
if (maxFiles !== null && files.length >= maxFiles) return;
|
|
354
|
-
try {
|
|
355
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
356
|
-
for (const entry of entries) {
|
|
357
|
-
if (maxFiles !== null && files.length >= maxFiles) break;
|
|
358
|
-
const fullPath = path.join(dir, entry.name);
|
|
359
|
-
|
|
360
|
-
if (entry.isDirectory()) {
|
|
361
|
-
const relativePath = path.relative(basePath, fullPath);
|
|
362
|
-
if (slopAnalyzers.shouldExclude(relativePath, EXCLUDE_DIRS)) continue;
|
|
363
|
-
if (isIgnored && isIgnored(relativePath, true)) continue;
|
|
364
|
-
if (!entry.name.startsWith('.')) {
|
|
365
|
-
scan(fullPath);
|
|
366
|
-
}
|
|
367
|
-
} else if (entry.isFile()) {
|
|
368
|
-
const relativePath = path.relative(basePath, fullPath);
|
|
369
|
-
if (slopAnalyzers.shouldExclude(relativePath, EXCLUDE_DIRS)) continue;
|
|
370
|
-
if (isIgnored && isIgnored(relativePath, false)) continue;
|
|
371
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
372
|
-
if (extensions.includes(ext)) {
|
|
373
|
-
files.push(fullPath);
|
|
374
|
-
if (maxFiles !== null && files.length >= maxFiles) break;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
} catch {
|
|
379
|
-
// Skip directories we can't read
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
scan(basePath);
|
|
384
|
-
return files;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Collect files for all languages in a single walk.
|
|
389
|
-
* @param {string} basePath - Repository root
|
|
390
|
-
* @param {string[]} languages - Languages to collect
|
|
391
|
-
* @param {Object} options
|
|
392
|
-
* @param {number} [options.maxFiles] - Global file limit
|
|
393
|
-
* @returns {Map<string, string[]>} - Map of language -> file paths
|
|
394
|
-
*/
|
|
395
|
-
function collectFilesByLanguage(basePath, languages, options = {}) {
|
|
396
|
-
const langList = Array.isArray(languages) ? languages : [];
|
|
397
|
-
const filesByLanguage = new Map();
|
|
398
|
-
for (const lang of langList) {
|
|
399
|
-
filesByLanguage.set(lang, []);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const extensionToLang = new Map();
|
|
403
|
-
for (const lang of langList) {
|
|
404
|
-
const extensions = LANGUAGE_EXTENSIONS[lang] || [];
|
|
405
|
-
for (const ext of extensions) {
|
|
406
|
-
if (!extensionToLang.has(ext)) {
|
|
407
|
-
extensionToLang.set(ext, lang);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const isIgnored = slopAnalyzers.parseGitignore(basePath, fs, path);
|
|
413
|
-
const maxFiles = Number.isFinite(options.maxFiles) ? Math.max(0, Math.floor(options.maxFiles)) : null;
|
|
414
|
-
let count = 0;
|
|
415
|
-
|
|
416
|
-
function scan(dir) {
|
|
417
|
-
if (maxFiles !== null && count >= maxFiles) return;
|
|
418
|
-
try {
|
|
419
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
420
|
-
for (const entry of entries) {
|
|
421
|
-
if (maxFiles !== null && count >= maxFiles) break;
|
|
422
|
-
const fullPath = path.join(dir, entry.name);
|
|
423
|
-
|
|
424
|
-
if (entry.isDirectory()) {
|
|
425
|
-
const relativePath = path.relative(basePath, fullPath);
|
|
426
|
-
if (slopAnalyzers.shouldExclude(relativePath, EXCLUDE_DIRS)) continue;
|
|
427
|
-
if (isIgnored && isIgnored(relativePath, true)) continue;
|
|
428
|
-
if (!entry.name.startsWith('.')) {
|
|
429
|
-
scan(fullPath);
|
|
430
|
-
}
|
|
431
|
-
} else if (entry.isFile()) {
|
|
432
|
-
const relativePath = path.relative(basePath, fullPath);
|
|
433
|
-
if (slopAnalyzers.shouldExclude(relativePath, EXCLUDE_DIRS)) continue;
|
|
434
|
-
if (isIgnored && isIgnored(relativePath, false)) continue;
|
|
435
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
436
|
-
const lang = extensionToLang.get(ext);
|
|
437
|
-
if (lang) {
|
|
438
|
-
const bucket = filesByLanguage.get(lang);
|
|
439
|
-
if (bucket) {
|
|
440
|
-
bucket.push(fullPath);
|
|
441
|
-
count++;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
} catch {
|
|
447
|
-
// Skip directories we can't read
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
scan(basePath);
|
|
452
|
-
return filesByLanguage;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function createSymbolMaps() {
|
|
456
|
-
return {
|
|
457
|
-
exports: new Map(),
|
|
458
|
-
functions: new Map(),
|
|
459
|
-
classes: new Map(),
|
|
460
|
-
types: new Map(),
|
|
461
|
-
constants: new Map()
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Read multiple files asynchronously in batches
|
|
467
|
-
* @param {string[]} files - Array of file paths
|
|
468
|
-
* @param {number} batchSize - Concurrent reads per batch
|
|
469
|
-
* @returns {Promise<Map<string, {content: string, error: Error|null}>>}
|
|
470
|
-
*/
|
|
471
|
-
async function batchReadFiles(files, batchSize = FILE_READ_BATCH_SIZE) {
|
|
472
|
-
const results = new Map();
|
|
473
|
-
|
|
474
|
-
for (let i = 0; i < files.length; i += batchSize) {
|
|
475
|
-
const batch = files.slice(i, i + batchSize);
|
|
476
|
-
const batchResults = await Promise.all(
|
|
477
|
-
batch.map(async (file) => {
|
|
478
|
-
try {
|
|
479
|
-
const content = await fsPromises.readFile(file, 'utf8');
|
|
480
|
-
return { file, content, error: null };
|
|
481
|
-
} catch (err) {
|
|
482
|
-
return { file, content: null, error: err };
|
|
483
|
-
}
|
|
484
|
-
})
|
|
485
|
-
);
|
|
486
|
-
|
|
487
|
-
for (const result of batchResults) {
|
|
488
|
-
results.set(result.file, { content: result.content, error: result.error });
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
return results;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function chunkArray(items, size) {
|
|
496
|
-
if (!items || items.length === 0) return [];
|
|
497
|
-
const chunks = [];
|
|
498
|
-
for (let i = 0; i < items.length; i += size) {
|
|
499
|
-
chunks.push(items.slice(i, i + size));
|
|
500
|
-
}
|
|
501
|
-
return chunks;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function truncatePattern(pattern, max = 120) {
|
|
505
|
-
if (typeof pattern !== 'string') return '';
|
|
506
|
-
if (pattern.length <= max) return pattern;
|
|
507
|
-
return `${pattern.slice(0, max - 3)}...`;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function buildAstGrepError({ reason, pattern, lang, filePaths, basePath, stderr }) {
|
|
511
|
-
const batchLabel = Array.isArray(filePaths) && filePaths.length === 1
|
|
512
|
-
? normalizeMatchPath(filePaths[0], basePath)
|
|
513
|
-
: '[batch]';
|
|
514
|
-
|
|
515
|
-
const details = stderr && String(stderr).trim()
|
|
516
|
-
? ` (${String(stderr).trim()})`
|
|
517
|
-
: '';
|
|
518
|
-
|
|
519
|
-
return {
|
|
520
|
-
file: batchLabel,
|
|
521
|
-
error: `ast-grep ${reason} for ${lang}${details}`,
|
|
522
|
-
pattern: truncatePattern(pattern)
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function runAstGrepPatternAsync(cmd, pattern, lang, basePath, filePaths, options = {}) {
|
|
527
|
-
if (!pattern || !filePaths || filePaths.length === 0) {
|
|
528
|
-
return Promise.resolve([]);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
return new Promise((resolve) => {
|
|
532
|
-
const child = spawn(cmd, [
|
|
533
|
-
'run',
|
|
534
|
-
'--pattern', pattern,
|
|
535
|
-
'--lang', lang,
|
|
536
|
-
'--json=stream',
|
|
537
|
-
...filePaths
|
|
538
|
-
], {
|
|
539
|
-
cwd: basePath,
|
|
540
|
-
windowsHide: true,
|
|
541
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
let stdout = '';
|
|
545
|
-
let stderr = '';
|
|
546
|
-
let settled = false;
|
|
547
|
-
|
|
548
|
-
const timeoutHandle = setTimeout(() => {
|
|
549
|
-
if (settled) return;
|
|
550
|
-
settled = true;
|
|
551
|
-
child.kill();
|
|
552
|
-
if (typeof options.onError === 'function') {
|
|
553
|
-
options.onError(buildAstGrepError({
|
|
554
|
-
reason: 'timed out after 300000ms',
|
|
555
|
-
pattern,
|
|
556
|
-
lang,
|
|
557
|
-
filePaths,
|
|
558
|
-
basePath,
|
|
559
|
-
stderr
|
|
560
|
-
}));
|
|
561
|
-
}
|
|
562
|
-
resolve([]);
|
|
563
|
-
}, 300000);
|
|
564
|
-
|
|
565
|
-
child.stdout.on('data', (chunk) => {
|
|
566
|
-
stdout += chunk.toString();
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
child.stderr.on('data', (chunk) => {
|
|
570
|
-
stderr += chunk.toString();
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
child.on('error', (error) => {
|
|
574
|
-
if (settled) return;
|
|
575
|
-
settled = true;
|
|
576
|
-
clearTimeout(timeoutHandle);
|
|
577
|
-
if (typeof options.onError === 'function') {
|
|
578
|
-
options.onError(buildAstGrepError({
|
|
579
|
-
reason: 'execution failed',
|
|
580
|
-
pattern,
|
|
581
|
-
lang,
|
|
582
|
-
filePaths,
|
|
583
|
-
basePath,
|
|
584
|
-
stderr: error.message
|
|
585
|
-
}));
|
|
586
|
-
}
|
|
587
|
-
resolve([]);
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
child.on('close', (code) => {
|
|
591
|
-
if (settled) return;
|
|
592
|
-
settled = true;
|
|
593
|
-
clearTimeout(timeoutHandle);
|
|
594
|
-
|
|
595
|
-
if (typeof code === 'number' && code > 1) {
|
|
596
|
-
if (typeof options.onError === 'function') {
|
|
597
|
-
options.onError(buildAstGrepError({
|
|
598
|
-
reason: `returned exit code ${code}`,
|
|
599
|
-
pattern,
|
|
600
|
-
lang,
|
|
601
|
-
filePaths,
|
|
602
|
-
basePath,
|
|
603
|
-
stderr
|
|
604
|
-
}));
|
|
605
|
-
}
|
|
606
|
-
resolve([]);
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
resolve(parseNdjson(stdout));
|
|
611
|
-
});
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
async function runAstGrepPatternBatches(cmd, pattern, lang, basePath, chunks, options = {}) {
|
|
616
|
-
const concurrency = Number.isFinite(options.concurrency)
|
|
617
|
-
? Math.max(1, Math.floor(options.concurrency))
|
|
618
|
-
: AST_GREP_CONCURRENCY;
|
|
619
|
-
|
|
620
|
-
return runWithConcurrency(chunks, concurrency, async (chunk) => {
|
|
621
|
-
return runAstGrepPatternAsync(cmd, pattern, lang, basePath, chunk, options);
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
function normalizeMatchPath(matchFile, basePath) {
|
|
626
|
-
if (!matchFile) return null;
|
|
627
|
-
const absolutePath = path.isAbsolute(matchFile) ? matchFile : path.join(basePath, matchFile);
|
|
628
|
-
return path.relative(basePath, absolutePath).replace(/\\/g, '/');
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
function addSymbolToMap(map, name, match, kind, extra = {}) {
|
|
632
|
-
if (!name) return;
|
|
633
|
-
if (!map.has(name)) {
|
|
634
|
-
map.set(name, {
|
|
635
|
-
name,
|
|
636
|
-
line: getLine(match),
|
|
637
|
-
kind,
|
|
638
|
-
...extra
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
function runAstGrepPattern(cmd, pattern, lang, basePath, filePaths, options = {}) {
|
|
644
|
-
if (!pattern || !filePaths || filePaths.length === 0) return [];
|
|
645
|
-
|
|
646
|
-
try {
|
|
647
|
-
const result = spawnSync(cmd, [
|
|
648
|
-
'run',
|
|
649
|
-
'--pattern', pattern,
|
|
650
|
-
'--lang', lang,
|
|
651
|
-
'--json=stream',
|
|
652
|
-
...filePaths
|
|
653
|
-
], {
|
|
654
|
-
cwd: basePath,
|
|
655
|
-
encoding: 'utf8',
|
|
656
|
-
timeout: 300000,
|
|
657
|
-
windowsHide: true,
|
|
658
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
if (result.error) {
|
|
662
|
-
if (typeof options.onError === 'function') {
|
|
663
|
-
options.onError(buildAstGrepError({
|
|
664
|
-
reason: 'execution failed',
|
|
665
|
-
pattern,
|
|
666
|
-
lang,
|
|
667
|
-
filePaths,
|
|
668
|
-
basePath,
|
|
669
|
-
stderr: result.error.message
|
|
670
|
-
}));
|
|
671
|
-
}
|
|
672
|
-
return [];
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
if (typeof result.status === 'number' && result.status > 1) {
|
|
676
|
-
if (typeof options.onError === 'function') {
|
|
677
|
-
options.onError(buildAstGrepError({
|
|
678
|
-
reason: `returned exit code ${result.status}`,
|
|
679
|
-
pattern,
|
|
680
|
-
lang,
|
|
681
|
-
filePaths,
|
|
682
|
-
basePath,
|
|
683
|
-
stderr: result.stderr
|
|
684
|
-
}));
|
|
685
|
-
}
|
|
686
|
-
return [];
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
return parseNdjson(result.stdout);
|
|
690
|
-
} catch (error) {
|
|
691
|
-
if (typeof options.onError === 'function') {
|
|
692
|
-
options.onError(buildAstGrepError({
|
|
693
|
-
reason: 'threw an exception',
|
|
694
|
-
pattern,
|
|
695
|
-
lang,
|
|
696
|
-
filePaths,
|
|
697
|
-
basePath,
|
|
698
|
-
stderr: error.message
|
|
699
|
-
}));
|
|
700
|
-
}
|
|
701
|
-
return [];
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
/**
|
|
706
|
-
* Extract symbols from a file using ast-grep
|
|
707
|
-
* @param {string} cmd - ast-grep command
|
|
708
|
-
* @param {string} file - File path
|
|
709
|
-
* @param {string} language - Language name
|
|
710
|
-
* @param {Object} langQueries - Query patterns for this language
|
|
711
|
-
* @param {string} basePath - Repository root (for cwd)
|
|
712
|
-
* @param {string} content - File content
|
|
713
|
-
* @returns {Object} - Extracted symbols
|
|
714
|
-
*/
|
|
715
|
-
function extractSymbols(cmd, file, language, langQueries, basePath, content, options = {}) {
|
|
716
|
-
const symbols = {
|
|
717
|
-
exports: [],
|
|
718
|
-
functions: [],
|
|
719
|
-
classes: [],
|
|
720
|
-
types: [],
|
|
721
|
-
constants: []
|
|
722
|
-
};
|
|
723
|
-
|
|
724
|
-
const sgLang = queries.getSgLanguageForFile(file, language);
|
|
725
|
-
|
|
726
|
-
const exportMap = new Map();
|
|
727
|
-
const functionMap = new Map();
|
|
728
|
-
const classMap = new Map();
|
|
729
|
-
const typeMap = new Map();
|
|
730
|
-
const constMap = new Map();
|
|
731
|
-
|
|
732
|
-
const addSymbol = (map, name, match, kind, extra = {}) => {
|
|
733
|
-
if (!name) return;
|
|
734
|
-
if (!map.has(name)) {
|
|
735
|
-
map.set(name, {
|
|
736
|
-
name,
|
|
737
|
-
line: getLine(match),
|
|
738
|
-
kind,
|
|
739
|
-
...extra
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
};
|
|
743
|
-
|
|
744
|
-
const runPatternSet = (patterns, targetMap, defaultKind) => {
|
|
745
|
-
if (!patterns) return;
|
|
746
|
-
for (const patternDef of patterns) {
|
|
747
|
-
const pattern = patternDef.pattern || patternDef;
|
|
748
|
-
const results = runAstGrep(cmd, file, pattern, sgLang, basePath, options);
|
|
749
|
-
for (const match of results) {
|
|
750
|
-
const names = extractNamesFromMatch(match, patternDef);
|
|
751
|
-
for (const name of names) {
|
|
752
|
-
const kind = patternDef.kind || defaultKind;
|
|
753
|
-
addSymbol(targetMap, name, match, kind, patternDef.extra);
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
};
|
|
758
|
-
|
|
759
|
-
// Extract exports
|
|
760
|
-
runPatternSet(langQueries.exports, exportMap, 'export');
|
|
761
|
-
|
|
762
|
-
// Extract functions
|
|
763
|
-
runPatternSet(langQueries.functions, functionMap, 'function');
|
|
764
|
-
|
|
765
|
-
// Extract classes
|
|
766
|
-
runPatternSet(langQueries.classes, classMap, 'class');
|
|
767
|
-
|
|
768
|
-
// Extract types
|
|
769
|
-
runPatternSet(langQueries.types, typeMap, 'type');
|
|
770
|
-
|
|
771
|
-
// Extract constants
|
|
772
|
-
runPatternSet(langQueries.constants, constMap, 'constant');
|
|
773
|
-
|
|
774
|
-
// Infer exports for languages with implicit public rules
|
|
775
|
-
const exportNames = new Set(exportMap.keys());
|
|
776
|
-
applyLanguageExportRules(language, content, exportNames, functionMap, classMap, typeMap, constMap);
|
|
777
|
-
|
|
778
|
-
// Ensure export entries exist for inferred exports
|
|
779
|
-
ensureExportEntries(exportMap, exportNames, functionMap, classMap, typeMap, constMap);
|
|
780
|
-
|
|
781
|
-
// Convert maps to arrays and mark exported flags
|
|
782
|
-
symbols.exports = mapToSortedArray(exportMap);
|
|
783
|
-
symbols.functions = mapToSortedArray(functionMap, exportNames);
|
|
784
|
-
symbols.classes = mapToSortedArray(classMap, exportNames);
|
|
785
|
-
symbols.types = mapToSortedArray(typeMap, exportNames);
|
|
786
|
-
symbols.constants = mapToSortedArray(constMap, exportNames);
|
|
787
|
-
|
|
788
|
-
return symbols;
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
/**
|
|
792
|
-
* Extract imports from a file using ast-grep
|
|
793
|
-
* @param {string} cmd - ast-grep command
|
|
794
|
-
* @param {string} file - File path
|
|
795
|
-
* @param {string} language - Language name
|
|
796
|
-
* @param {Object} langQueries - Query patterns for this language
|
|
797
|
-
* @param {string} basePath - Repository root (for cwd)
|
|
798
|
-
* @returns {Array} - Extracted imports
|
|
799
|
-
*/
|
|
800
|
-
function extractImports(cmd, file, language, langQueries, basePath, options = {}) {
|
|
801
|
-
const imports = [];
|
|
802
|
-
|
|
803
|
-
if (!langQueries.imports) return imports;
|
|
804
|
-
|
|
805
|
-
const sgLang = queries.getSgLanguageForFile(file, language);
|
|
806
|
-
const seen = new Set();
|
|
807
|
-
|
|
808
|
-
for (const patternDef of langQueries.imports) {
|
|
809
|
-
const pattern = patternDef.pattern || patternDef;
|
|
810
|
-
const results = runAstGrep(cmd, file, pattern, sgLang, basePath, options);
|
|
811
|
-
for (const match of results) {
|
|
812
|
-
const sourceResult = extractSourceFromMatch(match, patternDef);
|
|
813
|
-
const sources = Array.isArray(sourceResult) ? sourceResult : [sourceResult];
|
|
814
|
-
for (const source of sources) {
|
|
815
|
-
if (!source) continue;
|
|
816
|
-
const key = `${source}:${patternDef.kind || 'import'}`;
|
|
817
|
-
if (seen.has(key)) continue;
|
|
818
|
-
seen.add(key);
|
|
819
|
-
imports.push({
|
|
820
|
-
source,
|
|
821
|
-
kind: patternDef.kind || 'import',
|
|
822
|
-
line: getLine(match)
|
|
823
|
-
});
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
return imports;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
/**
|
|
832
|
-
* Run ast-grep with a pattern
|
|
833
|
-
* @param {string} cmd - ast-grep command
|
|
834
|
-
* @param {string} file - File to scan
|
|
835
|
-
* @param {string} pattern - Pattern to match
|
|
836
|
-
* @param {string} lang - ast-grep language identifier
|
|
837
|
-
* @param {string} basePath - Working directory
|
|
838
|
-
* @returns {Array} - Match results
|
|
839
|
-
*/
|
|
840
|
-
function runAstGrep(cmd, file, pattern, lang, basePath, options = {}) {
|
|
841
|
-
try {
|
|
842
|
-
const result = spawnSync(cmd, [
|
|
843
|
-
'run',
|
|
844
|
-
'--pattern', pattern,
|
|
845
|
-
'--lang', lang,
|
|
846
|
-
'--json=stream',
|
|
847
|
-
file
|
|
848
|
-
], {
|
|
849
|
-
cwd: basePath,
|
|
850
|
-
encoding: 'utf8',
|
|
851
|
-
timeout: 30000,
|
|
852
|
-
windowsHide: true,
|
|
853
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
if (result.error) {
|
|
857
|
-
if (typeof options.onError === 'function') {
|
|
858
|
-
options.onError(buildAstGrepError({
|
|
859
|
-
reason: 'execution failed',
|
|
860
|
-
pattern,
|
|
861
|
-
lang,
|
|
862
|
-
filePaths: [file],
|
|
863
|
-
basePath,
|
|
864
|
-
stderr: result.error.message
|
|
865
|
-
}));
|
|
866
|
-
}
|
|
867
|
-
return [];
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// ast-grep exits with 1 when no matches
|
|
871
|
-
if (typeof result.status === 'number' && result.status > 1) {
|
|
872
|
-
if (typeof options.onError === 'function') {
|
|
873
|
-
options.onError(buildAstGrepError({
|
|
874
|
-
reason: `returned exit code ${result.status}`,
|
|
875
|
-
pattern,
|
|
876
|
-
lang,
|
|
877
|
-
filePaths: [file],
|
|
878
|
-
basePath,
|
|
879
|
-
stderr: result.stderr
|
|
880
|
-
}));
|
|
881
|
-
}
|
|
882
|
-
return [];
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
return parseNdjson(result.stdout);
|
|
886
|
-
} catch (error) {
|
|
887
|
-
if (typeof options.onError === 'function') {
|
|
888
|
-
options.onError(buildAstGrepError({
|
|
889
|
-
reason: 'threw an exception',
|
|
890
|
-
pattern,
|
|
891
|
-
lang,
|
|
892
|
-
filePaths: [file],
|
|
893
|
-
basePath,
|
|
894
|
-
stderr: error.message
|
|
895
|
-
}));
|
|
896
|
-
}
|
|
897
|
-
return [];
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
function parseNdjson(output) {
|
|
902
|
-
const matches = [];
|
|
903
|
-
const lines = (output || '').split('\n').filter(Boolean);
|
|
904
|
-
for (const line of lines) {
|
|
905
|
-
try {
|
|
906
|
-
matches.push(JSON.parse(line));
|
|
907
|
-
} catch {
|
|
908
|
-
// Skip malformed lines
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
return matches;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
/**
|
|
915
|
-
* Extract names from an ast-grep match, based on pattern metadata
|
|
916
|
-
* @param {Object} match - ast-grep match result
|
|
917
|
-
* @param {Object|string} patternDef - Pattern definition or string
|
|
918
|
-
* @returns {string[]} - List of names
|
|
919
|
-
*/
|
|
920
|
-
function extractNamesFromMatch(match, patternDef) {
|
|
921
|
-
const def = typeof patternDef === 'string' ? {} : (patternDef || {});
|
|
922
|
-
|
|
923
|
-
if (def.multi === 'exportList') {
|
|
924
|
-
return extractNamesFromExportList(match.text || '');
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
if (def.multi === 'objectLiteral') {
|
|
928
|
-
return extractNamesFromObjectLiteral(match.text || '');
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
const name = extractNameFromMatch(match, def.nameVar);
|
|
932
|
-
if (name) return [name];
|
|
933
|
-
if (def.fallbackName) return [def.fallbackName];
|
|
934
|
-
return [];
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
function getMetaVariable(match, key) {
|
|
938
|
-
if (!match || !match.metaVariables) return null;
|
|
939
|
-
if (match.metaVariables[key]) return match.metaVariables[key];
|
|
940
|
-
if (match.metaVariables.single && match.metaVariables.single[key]) {
|
|
941
|
-
return match.metaVariables.single[key];
|
|
942
|
-
}
|
|
943
|
-
return null;
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
/**
|
|
947
|
-
* Extract a single name from ast-grep match
|
|
948
|
-
* @param {Object} match - ast-grep match result
|
|
949
|
-
* @param {string|string[]} nameVar - Preferred meta variable name(s)
|
|
950
|
-
* @returns {string|null}
|
|
951
|
-
*/
|
|
952
|
-
function extractNameFromMatch(match, nameVar) {
|
|
953
|
-
const vars = [];
|
|
954
|
-
if (Array.isArray(nameVar)) {
|
|
955
|
-
vars.push(...nameVar);
|
|
956
|
-
} else if (nameVar) {
|
|
957
|
-
vars.push(nameVar);
|
|
958
|
-
}
|
|
959
|
-
vars.push('NAME', 'FUNC', 'CLASS', 'IDENT', 'N');
|
|
960
|
-
|
|
961
|
-
for (const key of vars) {
|
|
962
|
-
const variable = getMetaVariable(match, key);
|
|
963
|
-
if (variable && variable.text) {
|
|
964
|
-
return variable.text;
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// Fallback: extract from matched text
|
|
969
|
-
if (match.text) {
|
|
970
|
-
const nameMatch = match.text.match(/(?:function|class|const|let|var|def|fn|pub\s+fn|type|struct|enum|trait|interface|record)\s+([a-zA-Z_][a-zA-Z0-9_]*)/);
|
|
971
|
-
if (nameMatch) {
|
|
972
|
-
return nameMatch[1];
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
return null;
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
/**
|
|
980
|
-
* Extract import source from ast-grep match
|
|
981
|
-
* @param {Object} match - ast-grep match result
|
|
982
|
-
* @param {Object|string} patternDef - Pattern definition or string
|
|
983
|
-
* @returns {string|null}
|
|
984
|
-
*/
|
|
985
|
-
function extractSourceFromMatch(match, patternDef) {
|
|
986
|
-
const def = typeof patternDef === 'string' ? {} : (patternDef || {});
|
|
987
|
-
const sourceVar = def.sourceVar || 'SOURCE';
|
|
988
|
-
|
|
989
|
-
const variable = getMetaVariable(match, sourceVar);
|
|
990
|
-
if (variable && variable.text) {
|
|
991
|
-
const raw = variable.text.replace(/^['"]|['"]$/g, '');
|
|
992
|
-
if (def.multiSource) {
|
|
993
|
-
return splitMultiSource(raw);
|
|
994
|
-
}
|
|
995
|
-
return raw;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
// Fallback: extract quoted string from match
|
|
999
|
-
if (match.text) {
|
|
1000
|
-
const sourceMatch = match.text.match(/['"]([^'"]+)['"]/);
|
|
1001
|
-
if (sourceMatch) {
|
|
1002
|
-
return sourceMatch[1];
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
return null;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
/**
|
|
1010
|
-
* Extract export names from `export { ... }`
|
|
1011
|
-
* @param {string} text - Match text
|
|
1012
|
-
* @returns {string[]}
|
|
1013
|
-
*/
|
|
1014
|
-
function extractNamesFromExportList(text) {
|
|
1015
|
-
const match = text.match(/\{([^}]+)\}/);
|
|
1016
|
-
if (!match) return [];
|
|
1017
|
-
|
|
1018
|
-
const names = new Set();
|
|
1019
|
-
const parts = match[1].split(',');
|
|
1020
|
-
for (const part of parts) {
|
|
1021
|
-
const trimmed = part.trim();
|
|
1022
|
-
if (!trimmed) continue;
|
|
1023
|
-
const aliasMatch = trimmed.split(/\s+as\s+/i).map(s => s.trim());
|
|
1024
|
-
const name = (aliasMatch[1] || aliasMatch[0]).replace(/[^a-zA-Z0-9_\$]/g, '');
|
|
1025
|
-
if (isValidIdentifier(name)) names.add(name);
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
return Array.from(names);
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
/**
|
|
1032
|
-
* Extract property names from object literal
|
|
1033
|
-
* @param {string} text - Match text
|
|
1034
|
-
* @returns {string[]}
|
|
1035
|
-
*/
|
|
1036
|
-
function extractNamesFromObjectLiteral(text) {
|
|
1037
|
-
const match = text.match(/\{([\s\S]*?)\}/);
|
|
1038
|
-
if (!match) return [];
|
|
1039
|
-
|
|
1040
|
-
const body = match[1];
|
|
1041
|
-
const names = new Set();
|
|
1042
|
-
|
|
1043
|
-
// Match shorthand properties and key: value pairs
|
|
1044
|
-
const propRegex = /\b([A-Za-z_$][\w$]*)\b\s*(?=,|\}|:)/g;
|
|
1045
|
-
let propMatch;
|
|
1046
|
-
while ((propMatch = propRegex.exec(body)) !== null) {
|
|
1047
|
-
const name = propMatch[1];
|
|
1048
|
-
if (isValidIdentifier(name)) names.add(name);
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
return Array.from(names);
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
/**
|
|
1055
|
-
* Split comma-separated import sources
|
|
1056
|
-
* @param {string} raw - Raw source text
|
|
1057
|
-
* @returns {string[]}
|
|
1058
|
-
*/
|
|
1059
|
-
function splitMultiSource(raw) {
|
|
1060
|
-
if (!raw) return [];
|
|
1061
|
-
const parts = raw.split(',').map(p => p.trim()).filter(Boolean);
|
|
1062
|
-
const results = [];
|
|
1063
|
-
for (const part of parts) {
|
|
1064
|
-
const [name] = part.split(/\s+as\s+/i);
|
|
1065
|
-
const cleaned = name.trim().replace(/^['"]|['"]$/g, '');
|
|
1066
|
-
if (cleaned) results.push(cleaned);
|
|
1067
|
-
}
|
|
1068
|
-
return results;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
/**
|
|
1072
|
-
* Determine if a name is a valid identifier
|
|
1073
|
-
* @param {string} name - Name to check
|
|
1074
|
-
* @returns {boolean}
|
|
1075
|
-
*/
|
|
1076
|
-
function isValidIdentifier(name) {
|
|
1077
|
-
return Boolean(name) && /^[A-Za-z_$][\w$]*$/.test(name);
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
/**
|
|
1081
|
-
* Get 1-based line number from ast-grep match
|
|
1082
|
-
* @param {Object} match - ast-grep match
|
|
1083
|
-
* @returns {number|null}
|
|
1084
|
-
*/
|
|
1085
|
-
function getLine(match) {
|
|
1086
|
-
const line = match?.range?.start?.line;
|
|
1087
|
-
return typeof line === 'number' ? line + 1 : null;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
/**
|
|
1091
|
-
* Apply language-specific export rules
|
|
1092
|
-
* @param {string} language - Language name
|
|
1093
|
-
* @param {string} content - File content
|
|
1094
|
-
* @param {Set<string>} exportNames - Export name set (in-place)
|
|
1095
|
-
* @param {Map} functionMap - Function symbols
|
|
1096
|
-
* @param {Map} classMap - Class symbols
|
|
1097
|
-
* @param {Map} typeMap - Type symbols
|
|
1098
|
-
* @param {Map} constMap - Constant symbols
|
|
1099
|
-
*/
|
|
1100
|
-
function applyLanguageExportRules(language, content, exportNames, functionMap, classMap, typeMap, constMap) {
|
|
1101
|
-
if (language === 'python') {
|
|
1102
|
-
const explicit = extractPythonAll(content);
|
|
1103
|
-
if (explicit.length > 0) {
|
|
1104
|
-
for (const name of explicit) exportNames.add(name);
|
|
1105
|
-
} else {
|
|
1106
|
-
addPublicNames(exportNames, functionMap, classMap, typeMap, constMap, name => !name.startsWith('_'));
|
|
1107
|
-
}
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
if (language === 'go') {
|
|
1112
|
-
addPublicNames(exportNames, functionMap, classMap, typeMap, constMap, name => isExportedGoName(name));
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
/**
|
|
1117
|
-
* Extract __all__ exports from Python content
|
|
1118
|
-
* @param {string} content - File content
|
|
1119
|
-
* @returns {string[]}
|
|
1120
|
-
*/
|
|
1121
|
-
function extractPythonAll(content) {
|
|
1122
|
-
if (!content) return [];
|
|
1123
|
-
const match = content.match(/__all__\s*=\s*[\[(]([\s\S]*?)[\])]\s*/m);
|
|
1124
|
-
if (!match) return [];
|
|
1125
|
-
|
|
1126
|
-
const body = match[1];
|
|
1127
|
-
const names = [];
|
|
1128
|
-
const stringRegex = /['"]([^'"]+)['"]/g;
|
|
1129
|
-
let m;
|
|
1130
|
-
while ((m = stringRegex.exec(body)) !== null) {
|
|
1131
|
-
if (m[1]) names.push(m[1]);
|
|
1132
|
-
}
|
|
1133
|
-
return names;
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
/**
|
|
1137
|
-
* Add public names from symbol maps based on predicate
|
|
1138
|
-
* @param {Set<string>} exportNames - Export name set
|
|
1139
|
-
* @param {...Map} maps - Symbol maps
|
|
1140
|
-
* @param {Function} predicate - Function(name) => boolean
|
|
1141
|
-
*/
|
|
1142
|
-
function addPublicNames(exportNames, ...args) {
|
|
1143
|
-
const predicate = args.pop();
|
|
1144
|
-
const maps = args;
|
|
1145
|
-
for (const map of maps) {
|
|
1146
|
-
for (const name of map.keys()) {
|
|
1147
|
-
if (predicate(name)) exportNames.add(name);
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
/**
|
|
1153
|
-
* Determine if a Go identifier is exported
|
|
1154
|
-
* @param {string} name - Identifier
|
|
1155
|
-
* @returns {boolean}
|
|
1156
|
-
*/
|
|
1157
|
-
function isExportedGoName(name) {
|
|
1158
|
-
if (!name) return false;
|
|
1159
|
-
const first = name[0];
|
|
1160
|
-
return first.toUpperCase() === first && first.toLowerCase() !== first;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
/**
|
|
1164
|
-
* Ensure export entries exist for inferred exports
|
|
1165
|
-
* @param {Map} exportMap - Export map to populate
|
|
1166
|
-
* @param {Set<string>} exportNames - Names to ensure
|
|
1167
|
-
* @param {Map} functionMap - Function map
|
|
1168
|
-
* @param {Map} classMap - Class map
|
|
1169
|
-
* @param {Map} typeMap - Type map
|
|
1170
|
-
* @param {Map} constMap - Constant map
|
|
1171
|
-
*/
|
|
1172
|
-
function ensureExportEntries(exportMap, exportNames, functionMap, classMap, typeMap, constMap) {
|
|
1173
|
-
const sources = [functionMap, classMap, typeMap, constMap];
|
|
1174
|
-
|
|
1175
|
-
for (const name of exportNames) {
|
|
1176
|
-
if (exportMap.has(name)) continue;
|
|
1177
|
-
|
|
1178
|
-
let entry = null;
|
|
1179
|
-
for (const map of sources) {
|
|
1180
|
-
if (map.has(name)) {
|
|
1181
|
-
const item = map.get(name);
|
|
1182
|
-
entry = { name, line: item.line, kind: item.kind };
|
|
1183
|
-
break;
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
if (!entry) {
|
|
1188
|
-
entry = { name, line: null, kind: 'export' };
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
exportMap.set(name, entry);
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
/**
|
|
1196
|
-
* Convert symbol map to sorted array and mark exported flags
|
|
1197
|
-
* @param {Map} map - Symbol map
|
|
1198
|
-
* @param {Set<string>} [exportNames] - Export name set
|
|
1199
|
-
* @returns {Array}
|
|
1200
|
-
*/
|
|
1201
|
-
function mapToSortedArray(map, exportNames) {
|
|
1202
|
-
const list = Array.from(map.values());
|
|
1203
|
-
if (exportNames) {
|
|
1204
|
-
for (const item of list) {
|
|
1205
|
-
item.exported = exportNames.has(item.name);
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
list.sort((a, b) => a.name.localeCompare(b.name));
|
|
1209
|
-
return list;
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
/**
|
|
1213
|
-
* Get git info for the repository
|
|
1214
|
-
* @param {string} basePath - Repository root
|
|
1215
|
-
* @returns {Object|null}
|
|
1216
|
-
*/
|
|
1217
|
-
function getGitInfo(basePath) {
|
|
1218
|
-
try {
|
|
1219
|
-
const commit = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
1220
|
-
cwd: basePath,
|
|
1221
|
-
encoding: 'utf8',
|
|
1222
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
1223
|
-
}).trim();
|
|
1224
|
-
|
|
1225
|
-
const branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
1226
|
-
cwd: basePath,
|
|
1227
|
-
encoding: 'utf8',
|
|
1228
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
1229
|
-
}).trim();
|
|
1230
|
-
|
|
1231
|
-
return { commit, branch };
|
|
1232
|
-
} catch {
|
|
1233
|
-
return null;
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
/**
|
|
1238
|
-
* Detect primary project type from languages
|
|
1239
|
-
* @param {string[]} languages - Detected languages
|
|
1240
|
-
* @returns {string}
|
|
1241
|
-
*/
|
|
1242
|
-
function detectProjectType(languages) {
|
|
1243
|
-
// Priority order
|
|
1244
|
-
const priority = ['typescript', 'javascript', 'python', 'rust', 'go', 'java'];
|
|
1245
|
-
for (const lang of priority) {
|
|
1246
|
-
if (languages.includes(lang)) {
|
|
1247
|
-
return lang === 'typescript' ? 'node' : lang;
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
return languages[0] || 'unknown';
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
/**
|
|
1254
|
-
* Scan a single file (for incremental updates)
|
|
1255
|
-
* @param {string} cmd - ast-grep command
|
|
1256
|
-
* @param {string} file - File path
|
|
1257
|
-
* @param {string} basePath - Repository root
|
|
1258
|
-
* @returns {Object|null} - File data or null if failed
|
|
1259
|
-
*/
|
|
1260
|
-
function scanSingleFile(cmd, file, basePath, options = {}) {
|
|
1261
|
-
const ext = path.extname(file).toLowerCase();
|
|
1262
|
-
|
|
1263
|
-
// Find language for this extension
|
|
1264
|
-
let language = null;
|
|
1265
|
-
for (const [lang, exts] of Object.entries(LANGUAGE_EXTENSIONS)) {
|
|
1266
|
-
if (exts.includes(ext)) {
|
|
1267
|
-
language = lang;
|
|
1268
|
-
break;
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
if (!language) return null;
|
|
1273
|
-
|
|
1274
|
-
const langQueries = queries.getQueriesForLanguage(language);
|
|
1275
|
-
if (!langQueries) return null;
|
|
1276
|
-
|
|
1277
|
-
try {
|
|
1278
|
-
const content = fs.readFileSync(file, 'utf8');
|
|
1279
|
-
const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
1280
|
-
|
|
1281
|
-
const symbols = extractSymbols(cmd, file, language, langQueries, basePath, content, options);
|
|
1282
|
-
const imports = extractImports(cmd, file, language, langQueries, basePath, options);
|
|
1283
|
-
|
|
1284
|
-
return {
|
|
1285
|
-
hash,
|
|
1286
|
-
language,
|
|
1287
|
-
size: content.length,
|
|
1288
|
-
symbols,
|
|
1289
|
-
imports
|
|
1290
|
-
};
|
|
1291
|
-
} catch (error) {
|
|
1292
|
-
if (typeof options.onError === 'function') {
|
|
1293
|
-
options.onError({
|
|
1294
|
-
file: normalizeMatchPath(file, basePath) || file,
|
|
1295
|
-
error: `Failed to scan file: ${error.message}`
|
|
1296
|
-
});
|
|
1297
|
-
}
|
|
1298
|
-
return null;
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
/**
|
|
1303
|
-
* Scan a single file asynchronously (for incremental updates)
|
|
1304
|
-
* Uses async file read, but ast-grep subprocess remains synchronous
|
|
1305
|
-
* @param {string} cmd - ast-grep command
|
|
1306
|
-
* @param {string} file - File path
|
|
1307
|
-
* @param {string} basePath - Repository root
|
|
1308
|
-
* @returns {Promise<Object|null>} - File data or null if failed
|
|
1309
|
-
*/
|
|
1310
|
-
async function scanSingleFileAsync(cmd, file, basePath, options = {}) {
|
|
1311
|
-
const ext = path.extname(file).toLowerCase();
|
|
1312
|
-
|
|
1313
|
-
// Find language for this extension
|
|
1314
|
-
let language = null;
|
|
1315
|
-
for (const [lang, exts] of Object.entries(LANGUAGE_EXTENSIONS)) {
|
|
1316
|
-
if (exts.includes(ext)) {
|
|
1317
|
-
language = lang;
|
|
1318
|
-
break;
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
if (!language) return null;
|
|
1323
|
-
|
|
1324
|
-
const langQueries = queries.getQueriesForLanguage(language);
|
|
1325
|
-
if (!langQueries) return null;
|
|
1326
|
-
|
|
1327
|
-
try {
|
|
1328
|
-
const content = await fsPromises.readFile(file, 'utf8');
|
|
1329
|
-
const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
1330
|
-
|
|
1331
|
-
const symbols = extractSymbols(cmd, file, language, langQueries, basePath, content, options);
|
|
1332
|
-
const imports = extractImports(cmd, file, language, langQueries, basePath, options);
|
|
1333
|
-
|
|
1334
|
-
return {
|
|
1335
|
-
hash,
|
|
1336
|
-
language,
|
|
1337
|
-
size: content.length,
|
|
1338
|
-
symbols,
|
|
1339
|
-
imports
|
|
1340
|
-
};
|
|
1341
|
-
} catch (error) {
|
|
1342
|
-
if (typeof options.onError === 'function') {
|
|
1343
|
-
options.onError({
|
|
1344
|
-
file: normalizeMatchPath(file, basePath) || file,
|
|
1345
|
-
error: `Failed to scan file: ${error.message}`
|
|
1346
|
-
});
|
|
1347
|
-
}
|
|
1348
|
-
return null;
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
module.exports = {
|
|
1353
|
-
detectLanguages,
|
|
1354
|
-
fullScan,
|
|
1355
|
-
findFilesForLanguage,
|
|
1356
|
-
collectFilesByLanguage,
|
|
1357
|
-
scanSingleFile,
|
|
1358
|
-
scanSingleFileAsync,
|
|
1359
|
-
runAstGrep,
|
|
1360
|
-
getGitInfo,
|
|
1361
|
-
batchReadFiles,
|
|
1362
|
-
LANGUAGE_EXTENSIONS,
|
|
1363
|
-
EXCLUDE_DIRS
|
|
1364
|
-
};
|