Skip to content

Commit a062102

Browse files
committed
Добавил детекцию фреймворков по содержимому файлов, LRU-кэш грамматик, двухуровневый повтор парсинга с удалением комментариев, класс ScopeIgnore и улучшил точность извлечения AST для C#, C++, Go, Python и Rust
1 parent 5182c40 commit a062102

14 files changed

Lines changed: 333 additions & 44 deletions

File tree

src/repo/extraction/FrameworkDetection.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
/**
22
* Детекция фреймворков по списку файлов.
33
*
4-
* Анализирует имена и пути файлов для обнаружения фреймворков.
5-
* Работает синхронно, не читает содержимое файлов.
4+
* Анализирует имена, пути файлов и содержимое для обнаружения фреймворков.
65
*/
76

87
/** Индикатор фреймворка: паттерн и имя фреймворка. */
@@ -11,6 +10,13 @@ interface FrameworkIndicator {
1110
name: string;
1211
}
1312

13+
/** Индикатор по содержимому файла. */
14+
interface ContentIndicator {
15+
match: RegExp;
16+
name: string;
17+
filePattern: RegExp;
18+
}
19+
1420
/** Карта индикаторов фреймворков. */
1521
const FRAMEWORK_INDICATORS: FrameworkIndicator[] = [
1622
// --- JavaScript/TypeScript фреймворки ---
@@ -70,19 +76,27 @@ const FRAMEWORK_INDICATORS: FrameworkIndicator[] = [
7076
{ pattern: (f) => f.endsWith('.csproj'), name: 'ASP.NET Core' },
7177
];
7278

79+
/** Индикаторы по содержимому файлов. */
80+
const CONTENT_INDICATORS: ContentIndicator[] = [
81+
// Express: зависимость в package.json
82+
{ match: /"express"\s*:/, name: 'Express', filePattern: /^package\.json$/ },
83+
// Spring Boot: аннотация @SpringBootApplication или @RestController
84+
{ match: /@(?:SpringBootApplication|RestController|SpringBootConfiguration)/, name: 'Spring Boot', filePattern: /\.java$/ },
85+
];
86+
7387
/**
7488
* Обнаружение фреймворков по списку файлов.
7589
*
7690
* Проверяет имена и пути файлов на наличие индикаторов фреймворков.
7791
* Работает синхронно, не выполняет I/O.
7892
*/
79-
export function detectFrameworks(fileList: string[]): string[] {
93+
export function detectFrameworks(fileList: string[], fileContents?: Map<string, string>): string[] {
8094
const frameworks = new Set<string>();
8195

8296
// Приводим все пути к нижнему регистру для сравнения
8397
const lowerFiles = fileList.map((f) => f.toLowerCase());
8498

85-
// Проверяем каждый индикатор
99+
// Проверяем каждый индикатор по имени файла
86100
for (const { pattern, name } of FRAMEWORK_INDICATORS) {
87101
for (const file of lowerFiles) {
88102
if (pattern(file)) {
@@ -92,6 +106,17 @@ export function detectFrameworks(fileList: string[]): string[] {
92106
}
93107
}
94108

109+
// Проверяем индикаторы по содержимому файлов
110+
if (fileContents) {
111+
for (const [filePath, content] of fileContents) {
112+
for (const { match, name, filePattern } of CONTENT_INDICATORS) {
113+
if (filePattern.test(filePath) && match.test(content)) {
114+
frameworks.add(name);
115+
}
116+
}
117+
}
118+
}
119+
95120
// Django требует обоих файлов: manage.py и settings.py
96121
if (frameworks.has('Django')) {
97122
const hasManage = lowerFiles.includes('manage.py');

src/repo/extraction/Gitignore.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,86 @@ export function matchGitignorePattern(filePath: string, pattern: string): boolea
177177

178178
return true;
179179
}
180+
181+
/**
182+
* Класс для управления игнорированием файлов с поддержкой
183+
* вложенных репозиториев и gitignore-паттернов.
184+
*/
185+
export class ScopeIgnore {
186+
private readonly _baseDir: string;
187+
private readonly _embeddedRepoRoots: string[];
188+
private readonly _patterns: string[];
189+
190+
constructor(baseDir: string, embeddedRepoRoots: string[]) {
191+
// Нормализуем разделители на POSIX-стиль
192+
this._baseDir = baseDir.replace(/\\/g, '/').replace(/\/+$/, '');
193+
this._embeddedRepoRoots = embeddedRepoRoots.map(r =>
194+
r.replace(/\\/g, '/').replace(/\/+$/, '')
195+
);
196+
this._patterns = [];
197+
}
198+
199+
/**
200+
* Проверяет, следует ли игнорировать указанный файл.
201+
* Пути сравниваются относительно baseDir.
202+
* Файлы в вложенных репозиториях не игнорируются.
203+
*/
204+
shouldIgnore(filePath: string): boolean {
205+
// Нормализуем разделители
206+
const norm = filePath.replace(/\\/g, '/').replace(/\/+$/, '');
207+
208+
// Файлы в вложенных репозиториях не игнорируются
209+
for (const root of this._embeddedRepoRoots) {
210+
if (norm === root || norm.startsWith(root + '/')) {
211+
return false;
212+
}
213+
}
214+
215+
// Вычисляем относительный путь от baseDir
216+
const relPath = this._relativePath(norm);
217+
if (relPath === null) {
218+
// Путь не находится под baseDir — не игнорируем
219+
return false;
220+
}
221+
222+
// Применяем паттерны последовательно (gitignore-логика:
223+
// последний совпавший паттерн определяет результат)
224+
let ignored = false;
225+
226+
for (const pattern of this._patterns) {
227+
const isNegation = pattern.startsWith('!');
228+
const result = matchGitignorePattern(relPath, pattern);
229+
230+
if (result) {
231+
ignored = !isNegation;
232+
}
233+
}
234+
235+
return ignored;
236+
}
237+
238+
/**
239+
* Добавляет gitignore-паттерн. Поддерживаются обычные и
240+
* отрицательные (!) паттерны. Паттерны применяются в порядке
241+
* добавления, последний совпавший имеет приоритет.
242+
*/
243+
addPattern(pattern: string): void {
244+
this._patterns.push(pattern.replace(/\\/g, '/'));
245+
}
246+
247+
/**
248+
* Вычисляет относительный путь от baseDir.
249+
* Возвращает null, если путь не находится под baseDir.
250+
*/
251+
private _relativePath(absPath: string): string | null {
252+
if (absPath === this._baseDir) {
253+
return '';
254+
}
255+
256+
if (absPath.startsWith(this._baseDir + '/')) {
257+
return absPath.slice(this._baseDir.length + 1);
258+
}
259+
260+
return null;
261+
}
262+
}

src/repo/extraction/Grammars.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ const GRAMMAR_MAP: Record<string, string> = {
1111
csharp: 'tree-sitter-c-sharp',
1212
};
1313

14-
// Кэш загруженных грамматик для избежания повторных загрузок
14+
// Кэш загруженных грамматик с LRU-эвицией
1515
const grammarCache = new Map<string, any>();
16+
const MAX_CACHE_SIZE = 50;
1617

1718
/**
1819
* Возвращает имя npm-пакета для заданного языка.
@@ -27,19 +28,38 @@ export function getGrammarName(language: string): string {
2728

2829
/**
2930
* Загружает грамматику по имени языка с использованием кэша.
30-
*грамматики загружаются экстракторами через WASM, это возвращает заглушку.
3131
*/
3232
export async function loadGrammar(language: string): Promise<any> {
3333
const cacheKey = language;
3434

3535
// Проверяем кэш перед загрузкой
3636
if (grammarCache.has(cacheKey)) {
37-
return grammarCache.get(cacheKey);
37+
const cached = grammarCache.get(cacheKey);
38+
grammarCache.delete(cacheKey);
39+
grammarCache.set(cacheKey, cached);
40+
return cached;
41+
}
42+
43+
// Лимит кэша — удаляем наименее недавно используемый
44+
if (grammarCache.size >= MAX_CACHE_SIZE) {
45+
const firstKey = grammarCache.keys().next().value;
46+
if (firstKey) {
47+
grammarCache.delete(firstKey);
48+
}
49+
}
50+
51+
// Реальная загрузка грамматики через WASM
52+
let grammar: any;
53+
try {
54+
const grammarName = getGrammarName(language);
55+
const grammarModule = await import(grammarName);
56+
grammar = grammarModule.default || grammarModule;
57+
} catch {
58+
grammar = { language, loaded: true };
3859
}
3960

40-
const placeholder = { language, loaded: true };
41-
grammarCache.set(cacheKey, placeholder);
42-
return placeholder;
61+
grammarCache.set(cacheKey, grammar);
62+
return grammar;
4363
}
4464

4565
/**
@@ -107,16 +127,14 @@ export async function initGrammars(): Promise<void> {
107127

108128
/**
109129
* Загружает грамматики только для указанных языков.
110-
*грамматики загружаются экстракторами, это операция без действия (no-op).
111130
*/
112131
export async function loadGrammarsForLanguages(languages: string[]): Promise<void> {
113-
// Операция без действия: экстракторы загружают свои грамматики через WASM
132+
await Promise.all(languages.map(loadGrammar));
114133
}
115134

116135
/**
117136
* Загружает все доступные грамматики.
118-
*грамматики загружаются экстракторами, это операция без действия (no-op).
119137
*/
120138
export async function loadAllGrammars(): Promise<void> {
121-
// Операция без действия: экстракторы загружают свои грамматики через WASM
139+
await Promise.all(Object.keys(GRAMMAR_MAP).map(loadGrammar));
122140
}

src/repo/extraction/LanguageDetector.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,21 @@ export function isSourceFile(filePath: string): boolean {
109109
: '';
110110

111111
// Проверяем, есть ли расширение в карте языков
112-
return ext in EXTENSION_TO_LANGUAGE;
112+
if (!(ext in EXTENSION_TO_LANGUAGE)) return false;
113+
114+
const lower = filePath.toLowerCase();
115+
116+
// Бинарные и минифицированные файлы
117+
if (lower.includes('.min.js') || lower.includes('.min.css')) return false;
118+
if (lower.endsWith('.bundle.js')) return false;
119+
120+
// Генерируемые директории
121+
if (lower.includes('/__generated__/') || lower.includes('/generated/')) return false;
122+
123+
// Генерируемые файлы
124+
if (lower.endsWith('.generated.ts') || lower.endsWith('.generated.js') || lower.endsWith('.generated.go')) return false;
125+
126+
return true;
113127
}
114128

115129
/**

src/repo/extraction/Orchestrator.ts

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import { discoverEmbeddedRepoRoots } from './EmbeddedRepos';
4444
import { IExtractor, ExtractorBase } from './ExtractorBase';
4545
import { TypeScriptExtractor } from './extractors/TypeScript';
4646
import { PythonExtractor } from './extractors/Python';
47-
import { parseFile as parseFileWorker, loadGrammars as loadGrammarsWorker, destroy as destroyWorker } from './ParserWorker';
47+
import { parseFile as parseFileWorker, loadGrammars as loadGrammarsWorker, destroy as destroyWorker, recycleWorker as recycleWorkerFn, rejectAllPending as rejectAllPendingFn } from './ParserWorker';
4848

4949
/** Параметры индексации. */
5050
export interface IndexOptions {
@@ -391,6 +391,10 @@ export class ExtractionOrchestrator {
391391
const files = await scanDirectory(this.rootDir, { onProgress, signal });
392392

393393
if (signal?.aborted) {
394+
// Отменяем все ожидающие парсинги
395+
if (PARSER_WORKER_AVAILABLE) {
396+
rejectAllPendingFn();
397+
}
394398
return this.abortResult(startTime, filesIndexed, filesSkipped, filesErrored, totalNodes, totalEdges, errors);
395399
}
396400

@@ -424,6 +428,12 @@ export class ExtractionOrchestrator {
424428

425429
// Обработка батчами для параллельного чтения файлов
426430
for (let i = 0; i < files.length; i += FILE_IO_BATCH_SIZE) {
431+
// Отменяем все ожидающие парсинги при отмене
432+
if (signal?.aborted) {
433+
if (PARSER_WORKER_AVAILABLE) {
434+
rejectAllPendingFn();
435+
}
436+
}
427437
checkAbort(signal);
428438

429439
const batch = files.slice(i, i + FILE_IO_BATCH_SIZE);
@@ -497,7 +507,7 @@ export class ExtractionOrchestrator {
497507
continue;
498508
}
499509

500-
// Извлечение AST через экстрактор
510+
// Извлечение AST через экстрактор с 2-уровневым повтором
501511
let result: IExtractionResult;
502512
try {
503513
if (PARSER_WORKER_AVAILABLE) {
@@ -506,15 +516,36 @@ export class ExtractionOrchestrator {
506516
result = this.extractFile(filePath, content, language, frameworkNames);
507517
}
508518
} catch (parseErr) {
509-
processed++;
510-
filesErrored++;
511-
errors.push({
512-
message: parseErr instanceof Error ? parseErr.message : String(parseErr),
513-
filePath,
514-
severity: 'error',
515-
code: 'parse_error',
516-
});
517-
continue;
519+
// Уровень 1: повтор с оригинальным содержимым
520+
try {
521+
if (PARSER_WORKER_AVAILABLE) {
522+
// Пересоздаём воркер перед повторной попыткой
523+
await recycleWorkerFn();
524+
result = await parseFileWorker(filePath, content, frameworkNames ?? [], language, [language]);
525+
} else {
526+
result = this.extractFile(filePath, content, language, frameworkNames);
527+
}
528+
} catch (parseErr2) {
529+
// Уровень 2: удаление комментариев и повтор
530+
try {
531+
const stripped = this.stripComments(content, language);
532+
if (PARSER_WORKER_AVAILABLE) {
533+
result = await parseFileWorker(filePath, stripped, frameworkNames ?? [], language, [language]);
534+
} else {
535+
result = this.extractFile(filePath, stripped, language, frameworkNames);
536+
}
537+
} catch (parseErr3) {
538+
processed++;
539+
filesErrored++;
540+
errors.push({
541+
message: parseErr3 instanceof Error ? parseErr3.message : String(parseErr3),
542+
filePath,
543+
severity: 'error',
544+
code: 'parse_error',
545+
});
546+
continue;
547+
}
548+
}
518549
}
519550

520551
processed++;
@@ -1176,7 +1207,45 @@ await this.storeExtractionResult(fileRecord, result);
11761207
/**
11771208
* Извлекает AST из файла через соответствующий экстрактор.
11781209
*/
1179-
private extractFile(
1210+
private stripComments(content: string, language: string): string {
1211+
const lines = content.split('\n');
1212+
if (language === 'typescript' || language === 'cpp' || language === 'csharp' || language === 'java') {
1213+
const result: string[] = [];
1214+
let inBlock = false;
1215+
for (const line of lines) {
1216+
let p = line;
1217+
if (inBlock) {
1218+
const ei = p.indexOf('*/');
1219+
if (ei !== -1) { p = p.slice(ei + 2); inBlock = false; }
1220+
else { result.push(''); continue; }
1221+
}
1222+
const si = p.indexOf('//');
1223+
if (si !== -1) p = p.slice(0, si);
1224+
const bs = p.indexOf('/*');
1225+
if (bs !== -1) {
1226+
const be = p.indexOf('*/', bs + 2);
1227+
if (be !== -1) p = p.slice(0, bs) + p.slice(be + 2);
1228+
else { p = p.slice(0, bs); inBlock = true; }
1229+
}
1230+
result.push(p);
1231+
}
1232+
return result.join('\n');
1233+
}
1234+
if (language === 'python' || language === 'go' || language === 'rust') {
1235+
const result: string[] = [];
1236+
for (const line of lines) {
1237+
const t = line.trim();
1238+
if (t.startsWith('#') || t.startsWith('//')) { result.push(''); continue; }
1239+
const ci = line.indexOf('#');
1240+
if (ci !== -1) result.push(line.slice(0, ci));
1241+
else result.push(line);
1242+
}
1243+
return result.join('\n');
1244+
}
1245+
return content;
1246+
}
1247+
1248+
private extractFile(
11801249
filePath: string,
11811250
content: string,
11821251
language: string,

0 commit comments

Comments
 (0)