Skip to content

Commit 3f7d0ba

Browse files
committed
Добавлены рёбра returns/type_of/imports, заменён Module на File, добавлено реальное время извлечения и исправлены ложные срабатывания детекции фреймворков во всех экстракторах
1 parent a062102 commit 3f7d0ba

16 files changed

Lines changed: 540 additions & 525 deletions

src/repo/extraction/ExtractorBase.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ describe("extraction pipeline", () => {
4949
expect(detectLanguage("src/file.ts")).toBe("typescript")
5050
})
5151

52-
it("detects TypeScript from .tsx extension", () => {
53-
expect(detectLanguage("src/file.tsx")).toBe("typescript")
52+
it("detects TSX from .tsx extension", () => {
53+
expect(detectLanguage("src/file.tsx")).toBe("tsx")
5454
})
5555

5656
it("detects JavaScript from .js extension", () => {
57-
expect(detectLanguage("src/file.js")).toBe("typescript")
57+
expect(detectLanguage("src/file.js")).toBe("javascript")
5858
})
5959

6060
it("detects Python from .py extension", () => {

src/repo/extraction/FrameworkDetection.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,8 @@ const FRAMEWORK_INDICATORS: FrameworkIndicator[] = [
5050
// Django: наличие manage.py и settings.py
5151
{ pattern: (f) => f === 'manage.py' || f.endsWith('/settings.py') || f === 'settings.py', name: 'Django' },
5252

53-
// Flask: app.py с импортом Flask или requirements.txt с flask
54-
{ pattern: (f) => f === 'requirements.txt', name: 'Flask' },
55-
56-
// FastAPI: main.py с импортом FastAPI
57-
{ pattern: (f) => f === 'main.py', name: 'FastAPI' },
53+
// Flask: детекция по содержимому файла (import flask)
54+
// FastAPI: детекция по содержимому файла (import fastapi)
5855

5956
// --- Java фреймворки ---
6057

@@ -67,8 +64,7 @@ const FRAMEWORK_INDICATORS: FrameworkIndicator[] = [
6764

6865
// --- Rust фреймворки ---
6966

70-
// Actix-web, Axum: Cargo.toml с соответствующей зависимостью
71-
{ pattern: (f) => f === 'Cargo.toml', name: 'Actix-web' },
67+
// Actix-web, Axum: детекция по содержимому Cargo.toml
7268

7369
// --- C# фреймворки ---
7470

@@ -82,6 +78,12 @@ const CONTENT_INDICATORS: ContentIndicator[] = [
8278
{ match: /"express"\s*:/, name: 'Express', filePattern: /^package\.json$/ },
8379
// Spring Boot: аннотация @SpringBootApplication или @RestController
8480
{ match: /@(?:SpringBootApplication|RestController|SpringBootConfiguration)/, name: 'Spring Boot', filePattern: /\.java$/ },
81+
// Flask: импорт flask в Python файле
82+
{ match: /(?:^|\s)import\s+flask(?:\s|$|\.|:)/i, name: 'Flask', filePattern: /\.py$/ },
83+
// FastAPI: импорт fastapi в Python файле
84+
{ match: /(?:^|\s)import\s+fastapi(?:\s|$|\.|:)/i, name: 'FastAPI', filePattern: /\.py$/ },
85+
// Actix-web: зависимость actix-web в Cargo.toml
86+
{ match: /actix-web/, name: 'Actix-web', filePattern: /^Cargo\.toml$/ },
8587
];
8688

8789
/**

src/repo/extraction/Gitignore.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ interface IGitignorePatternResult {
7878
* Транслирует gitignore-паттерн в регулярное выражение.
7979
* Поддерживает *, **, ? и анкоринг.
8080
*/
81-
function patternToRegex(pattern: string): IGitignorePatternResult {
81+
function patternToRegex(pattern: string): IGitignorePatternResult | null {
8282
let isNegation = false;
8383

8484
if (pattern.startsWith('!')) {
@@ -147,9 +147,13 @@ function patternToRegex(pattern: string): IGitignorePatternResult {
147147
regexSource = '^' + regexSource + '(?:/.*)?$';
148148
}
149149

150-
const re = new RegExp(regexSource);
151-
152-
return { re, isNegation, dirOnly };
150+
try {
151+
const re = new RegExp(regexSource);
152+
return { re, isNegation, dirOnly };
153+
} catch {
154+
console.warn(`[Gitignore] Неверный regex для паттерна: ${pattern}`);
155+
return null;
156+
}
153157
}
154158

155159
/**
@@ -160,7 +164,16 @@ function patternToRegex(pattern: string): IGitignorePatternResult {
160164
export function matchGitignorePattern(filePath: string, pattern: string): boolean {
161165
const normalizedPath = filePath.replace(/\\/g, '/');
162166

163-
const { re, isNegation, dirOnly } = patternToRegex(pattern);
167+
// Оборачиваем в try-catch: невалидные паттерны игнорируются
168+
let result: IGitignorePatternResult | null;
169+
try {
170+
result = patternToRegex(pattern);
171+
} catch {
172+
console.warn(`[Gitignore] Ошибка обработки паттерна: ${pattern}`);
173+
return false;
174+
}
175+
if (!result) return false;
176+
const { re, isNegation, dirOnly } = result;
164177

165178
if (dirOnly) {
166179
// Паттерн только для директорий — путь должен заканчиваться / или

src/repo/extraction/Grammars.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const GRAMMAR_MAP: Record<string, string> = {
88
rust: 'tree-sitter-rust',
99
java: 'tree-sitter-java',
1010
cpp: 'tree-sitter-cpp',
11+
// Грамматика для языка C
12+
c: 'tree-sitter-c',
1113
csharp: 'tree-sitter-c-sharp',
1214
};
1315

src/repo/extraction/LanguageDetector.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { Language } from '../ntgraph/Types';
33
// Карта соответствия расширений файлов языкам программирования
44
export const EXTENSION_TO_LANGUAGE: Record<string, Language> = {
55
'.ts': 'typescript',
6-
'.tsx': 'typescript',
7-
'.js': 'typescript',
8-
'.jsx': 'typescript',
6+
'.tsx': 'tsx',
7+
'.js': 'javascript',
8+
'.jsx': 'jsx',
99
'.mjs': 'typescript',
1010
'.cjs': 'typescript',
1111
'.py': 'python',
@@ -32,7 +32,7 @@ export const EXTENSION_TO_LANGUAGE: Record<string, Language> = {
3232
'.scala': 'scala',
3333
'.sc': 'scala',
3434
'.lua': 'lua',
35-
'.luau': 'lua',
35+
'.luau': 'luau',
3636
'.m': 'objc',
3737
'.r': 'r',
3838
'.R': 'r',

src/repo/extraction/Orchestrator.ts

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -551,11 +551,11 @@ export class ExtractionOrchestrator {
551551
processed++;
552552

553553
// Создание записи файла
554-
const fileRecord: IFileRecord = {
555-
path: filePath,
556-
contentHash: hashContent(content),
557-
language,
558-
size: stats.size,
554+
const fileRecord: IFileRecord = {
555+
path: filePath,
556+
contentHash: hashContent(content),
557+
language: language as Language,
558+
size: stats.size,
559559
modifiedAt: stats.mtimeMs,
560560
indexedAt: Date.now(),
561561
nodeCount: result.nodes.length,
@@ -641,6 +641,15 @@ await this.storeExtractionResult(fileRecord, result);
641641

642642
onProgress?.({ phase: 'scanning', current: 0, total: 0, file: '', durationMs: 0 });
643643

644+
// Загрузка грамматик перед основным циклом извлечения
645+
if (PARSER_WORKER_AVAILABLE) {
646+
try {
647+
await loadGrammarsWorker(['typescript', 'python']);
648+
} catch {
649+
// Игнорируем ошибки загрузки грамматик
650+
}
651+
}
652+
644653
// Пытаемся использовать git для быстрого обнаружения изменений
645654
const gitChanges = getGitChangedFiles(this.rootDir);
646655

@@ -900,6 +909,9 @@ await this.storeExtractionResult(fileRecord, result);
900909
this._detectedFrameworks = null;
901910
const frameworkNames = this.ensureDetectedFrameworks(filePaths);
902911

912+
// Уровень 2: сборка списка файлов, которые не удалось распарсить
913+
const failedFiles: { filePath: string; content: string; language: string; stats: fs.Stats }[] = [];
914+
903915
const total = filePaths.length;
904916
let processed = 0;
905917

@@ -959,20 +971,33 @@ await this.storeExtractionResult(fileRecord, result);
959971
continue;
960972
}
961973

962-
// Извлечение AST через экстрактор
974+
// Извлечение AST через экстрактор с 2-уровневым повтором
963975
let result: IExtractionResult;
964976
try {
965977
result = this.extractFile(filePath, content, language, frameworkNames);
966978
} catch (parseErr) {
967-
processed++;
968-
filesErrored++;
969-
errors.push({
970-
message: parseErr instanceof Error ? parseErr.message : String(parseErr),
971-
filePath,
972-
severity: 'error',
973-
code: 'parse_error',
974-
});
975-
continue;
979+
// Уровень 1: повтор с оригинальным содержимым
980+
try {
981+
result = this.extractFile(filePath, content, language, frameworkNames);
982+
} catch (parseErr2) {
983+
// Уровень 2: удаление комментариев и повтор
984+
try {
985+
const stripped = this.stripComments(content, language);
986+
result = this.extractFile(filePath, stripped, language, frameworkNames);
987+
} catch (parseErr3) {
988+
// Сохраняем для повторной обработки с фолбэком
989+
failedFiles.push({ filePath, content, language, stats });
990+
processed++;
991+
filesErrored++;
992+
errors.push({
993+
message: parseErr3 instanceof Error ? parseErr3.message : String(parseErr3),
994+
filePath,
995+
severity: 'error',
996+
code: 'parse_error',
997+
});
998+
continue;
999+
}
1000+
}
9761001
}
9771002

9781003
processed++;
@@ -1015,6 +1040,49 @@ await this.storeExtractionResult(fileRecord, result);
10151040
}
10161041
}
10171042

1043+
// Уровень 2: повторная обработка файлов, которые не удалось распарсить, с фолбэком
1044+
for (const { filePath, content, language, stats } of failedFiles) {
1045+
// Фолбэк: пробовать каждый доступный экстрактор
1046+
let fallbackResult: IExtractionResult | null = null;
1047+
for (const [lang, extractor] of EXTRACTOR_MAP) {
1048+
try {
1049+
fallbackResult = extractor.extract(content, filePath, frameworkNames);
1050+
if (fallbackResult.nodes.length > 0) {
1051+
break;
1052+
}
1053+
} catch {
1054+
// Пробуем следующий экстрактор
1055+
}
1056+
}
1057+
1058+
if (fallbackResult && fallbackResult.nodes.length > 0) {
1059+
const fileRecord: IFileRecord = {
1060+
path: filePath,
1061+
contentHash: hashContent(content),
1062+
language: language as Language,
1063+
size: stats.size,
1064+
modifiedAt: stats.mtimeMs,
1065+
indexedAt: Date.now(),
1066+
nodeCount: fallbackResult.nodes.length,
1067+
errors: fallbackResult.errors.length > 0 ? fallbackResult.errors : undefined,
1068+
};
1069+
1070+
await this.storeExtractionResult(fileRecord, fallbackResult);
1071+
1072+
filesErrored--;
1073+
filesIndexed++;
1074+
totalNodes += fallbackResult.nodes.length;
1075+
totalEdges += fallbackResult.edges.length;
1076+
1077+
if (fallbackResult.errors.length > 0) {
1078+
for (const err of fallbackResult.errors) {
1079+
if (!err.filePath) err.filePath = filePath;
1080+
}
1081+
errors.push(...fallbackResult.errors);
1082+
}
1083+
}
1084+
}
1085+
10181086
return {
10191087
indexed: filesIndexed,
10201088
updated: 0,
@@ -1436,10 +1504,10 @@ await this.storeExtractionResult(fileRecord, result);
14361504
try {
14371505
const result = this.extractFile(filePath, content, language, frameworkNames);
14381506

1439-
const fileRecord: IFileRecord = {
1440-
path: filePath,
1441-
contentHash: hashContent(content),
1442-
language,
1507+
const fileRecord: IFileRecord = {
1508+
path: filePath,
1509+
contentHash: hashContent(content),
1510+
language: language as Language,
14431511
size: stats.size,
14441512
modifiedAt: stats.mtimeMs,
14451513
indexedAt: Date.now(),

src/repo/extraction/ParserWorker.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,14 @@ async function parseInProcess(
3535
language: string,
3636
content: string,
3737
filePath: string,
38+
frameworkNames: string[],
3839
languages: string[],
3940
): Promise<IExtractionResult> {
4041
const { loadGrammarsForLanguages } = await import('./Grammars');
4142
await loadGrammarsForLanguages(languages);
4243

4344
const { extractFromSource } = await import('./tree-sitter');
44-
return extractFromSource(filePath, content, language);
45+
return extractFromSource(filePath, content, language, frameworkNames);
4546
}
4647

4748
class ParserWorkerManager {
@@ -219,10 +220,43 @@ class ParserWorkerManager {
219220
}
220221

221222
private retryParse(id: number, req: PendingParse): void {
222-
// Повторная отправка запроса на парсинг в восстановленный воркер.
223+
// Генерируем новый ID запроса, так как старый устарел после отклонения.
224+
const newId = this.nextRequestId++;
225+
226+
// Вычисляем таймаут для повторного запроса.
227+
const timeout = calcTimeout(req.content);
228+
229+
// Устанавливаем таймаут для нового запроса.
230+
const timeoutId = setTimeout(() => {
231+
this.pendingParses.delete(newId);
232+
req.reject(new Error('Таймаут парсинга: ' + timeout + 'ms'));
233+
this.worker?.terminate().catch(() => {});
234+
}, timeout);
235+
236+
// Создаём новый PendingParse с обновлённым таймаутом, но теми же resolve/reject.
237+
const newPending: PendingParse = {
238+
resolve: (value) => {
239+
clearTimeout(timeoutId);
240+
req.resolve(value);
241+
},
242+
reject: (reason) => {
243+
clearTimeout(timeoutId);
244+
req.reject(reason);
245+
},
246+
timeout: timeoutId,
247+
filePath: req.filePath,
248+
content: req.content,
249+
frameworkNames: req.frameworkNames,
250+
language: req.language,
251+
};
252+
253+
// Добавляем новый запрос в карту ожидающих парсингов.
254+
this.pendingParses.set(newId, newPending);
255+
256+
// Отправляем запрос на парсинг в восстановленный воркер.
223257
this.worker!.postMessage({
224258
type: 'parse',
225-
id,
259+
id: newId,
226260
filePath: req.filePath,
227261
content: req.content,
228262
frameworkNames: req.frameworkNames,
@@ -347,7 +381,7 @@ class ParserWorkerManager {
347381
}
348382

349383
if (!this.workerThreadsAvailable) {
350-
return parseInProcess(language, content, filePath, languages);
384+
return parseInProcess(language, content, filePath, frameworkNames, languages);
351385
}
352386

353387
if (this.languages.length === 0 || languages.some(l => !this.languages.includes(l))) {

0 commit comments

Comments
 (0)