- Fix Float32Array embedder bug: was creating zero-filled array instead of copying data from TypedArray-like objects - Fix VSS search query: use subquery pattern so LIMIT applies before JOIN - Pin tree-sitter versions: remove caret ranges for ABI stability, add overrides to lock tree-sitter core at 0.22.4 - Lazy grammar loading: load native bindings on first use per language instead of all at startup, so one missing grammar doesn't affect others - Remove stale src/extraction/queries copy from copy-assets script Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
249 lines
6.8 KiB
TypeScript
249 lines
6.8 KiB
TypeScript
/**
|
|
* Grammar Loading and Caching
|
|
*
|
|
* Uses lazy per-language loading so one missing native grammar does not
|
|
* break extraction for all other languages.
|
|
*/
|
|
|
|
import Parser from 'tree-sitter';
|
|
import { Language } from '../types';
|
|
|
|
type GrammarLoader = () => unknown;
|
|
type GrammarLanguage = Exclude<Language, 'liquid' | 'unknown'>;
|
|
|
|
/**
|
|
* Lazy grammar loaders — each language's native binding is only loaded
|
|
* on first use, so a failure in one grammar doesn't affect others.
|
|
*/
|
|
const grammarLoaders: Record<GrammarLanguage, GrammarLoader> = {
|
|
typescript: () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require('tree-sitter-typescript').typescript;
|
|
},
|
|
tsx: () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require('tree-sitter-typescript').tsx;
|
|
},
|
|
javascript: () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require('tree-sitter-javascript');
|
|
},
|
|
jsx: () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require('tree-sitter-javascript');
|
|
},
|
|
python: () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require('tree-sitter-python');
|
|
},
|
|
go: () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require('tree-sitter-go');
|
|
},
|
|
rust: () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require('tree-sitter-rust');
|
|
},
|
|
java: () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require('tree-sitter-java');
|
|
},
|
|
c: () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require('tree-sitter-c');
|
|
},
|
|
cpp: () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require('tree-sitter-cpp');
|
|
},
|
|
csharp: () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require('tree-sitter-c-sharp');
|
|
},
|
|
php: () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require('tree-sitter-php').php;
|
|
},
|
|
ruby: () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require('tree-sitter-ruby');
|
|
},
|
|
swift: () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require('tree-sitter-swift');
|
|
},
|
|
kotlin: () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require('tree-sitter-kotlin');
|
|
},
|
|
dart: () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require('@sengac/tree-sitter-dart');
|
|
},
|
|
// Note: tree-sitter-liquid has ABI compatibility issues with tree-sitter 0.22+
|
|
// Liquid extraction is handled separately via regex in tree-sitter.ts
|
|
};
|
|
|
|
/**
|
|
* File extension to Language mapping
|
|
*/
|
|
export const EXTENSION_MAP: Record<string, Language> = {
|
|
'.ts': 'typescript',
|
|
'.tsx': 'tsx',
|
|
'.js': 'javascript',
|
|
'.mjs': 'javascript',
|
|
'.cjs': 'javascript',
|
|
'.jsx': 'jsx',
|
|
'.py': 'python',
|
|
'.pyw': 'python',
|
|
'.go': 'go',
|
|
'.rs': 'rust',
|
|
'.java': 'java',
|
|
'.c': 'c',
|
|
'.h': 'c', // Could also be C++, defaulting to C
|
|
'.cpp': 'cpp',
|
|
'.cc': 'cpp',
|
|
'.cxx': 'cpp',
|
|
'.hpp': 'cpp',
|
|
'.hxx': 'cpp',
|
|
'.cs': 'csharp',
|
|
'.php': 'php',
|
|
'.rb': 'ruby',
|
|
'.rake': 'ruby',
|
|
'.swift': 'swift',
|
|
'.kt': 'kotlin',
|
|
'.kts': 'kotlin',
|
|
'.dart': 'dart',
|
|
'.liquid': 'liquid',
|
|
};
|
|
|
|
/**
|
|
* Caches for loaded grammars and parsers
|
|
*/
|
|
const parserCache = new Map<Language, Parser>();
|
|
const grammarCache = new Map<Language, unknown | null>();
|
|
const unavailableGrammarErrors = new Map<Language, string>();
|
|
|
|
/**
|
|
* Load a grammar on demand, caching the result.
|
|
* Returns null if the grammar is not available on this platform.
|
|
*/
|
|
function loadGrammar(language: Language): unknown | null {
|
|
if (grammarCache.has(language)) {
|
|
return grammarCache.get(language) ?? null;
|
|
}
|
|
|
|
const loader = grammarLoaders[language as GrammarLanguage];
|
|
if (!loader) {
|
|
grammarCache.set(language, null);
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const grammar = loader();
|
|
if (!grammar) {
|
|
throw new Error(`Grammar loader returned empty value for ${language}`);
|
|
}
|
|
grammarCache.set(language, grammar);
|
|
return grammar;
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
console.warn(`[CodeGraph] Failed to load ${language} grammar — parsing will be unavailable: ${message}`);
|
|
unavailableGrammarErrors.set(language, message);
|
|
grammarCache.set(language, null);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a parser for the specified language
|
|
*/
|
|
export function getParser(language: Language): Parser | null {
|
|
if (parserCache.has(language)) {
|
|
return parserCache.get(language)!;
|
|
}
|
|
|
|
const grammar = loadGrammar(language);
|
|
if (!grammar) {
|
|
return null;
|
|
}
|
|
|
|
const parser = new Parser();
|
|
parser.setLanguage(grammar as Parameters<typeof parser.setLanguage>[0]);
|
|
parserCache.set(language, parser);
|
|
return parser;
|
|
}
|
|
|
|
/**
|
|
* Detect language from file extension
|
|
*/
|
|
export function detectLanguage(filePath: string): Language {
|
|
const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
|
|
return EXTENSION_MAP[ext] || 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Check if a language is supported by currently available parsers.
|
|
*/
|
|
export function isLanguageSupported(language: Language): boolean {
|
|
if (language === 'liquid') return true; // custom regex extractor
|
|
if (language === 'unknown') return false;
|
|
return loadGrammar(language) !== null;
|
|
}
|
|
|
|
/**
|
|
* Get all currently supported languages.
|
|
*/
|
|
export function getSupportedLanguages(): Language[] {
|
|
const available = (Object.keys(grammarLoaders) as GrammarLanguage[])
|
|
.filter((language) => loadGrammar(language) !== null);
|
|
return [...available, 'liquid'];
|
|
}
|
|
|
|
/**
|
|
* Clear parser/grammar caches (useful for testing)
|
|
*/
|
|
export function clearParserCache(): void {
|
|
parserCache.clear();
|
|
grammarCache.clear();
|
|
unavailableGrammarErrors.clear();
|
|
}
|
|
|
|
/**
|
|
* Report grammars that failed to load.
|
|
*/
|
|
export function getUnavailableGrammarErrors(): Partial<Record<Language, string>> {
|
|
const out: Partial<Record<Language, string>> = {};
|
|
for (const [language, message] of unavailableGrammarErrors.entries()) {
|
|
out[language] = message;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Get language display name
|
|
*/
|
|
export function getLanguageDisplayName(language: Language): string {
|
|
const names: Record<Language, string> = {
|
|
typescript: 'TypeScript',
|
|
javascript: 'JavaScript',
|
|
tsx: 'TypeScript (TSX)',
|
|
jsx: 'JavaScript (JSX)',
|
|
python: 'Python',
|
|
go: 'Go',
|
|
rust: 'Rust',
|
|
java: 'Java',
|
|
c: 'C',
|
|
cpp: 'C++',
|
|
csharp: 'C#',
|
|
php: 'PHP',
|
|
ruby: 'Ruby',
|
|
swift: 'Swift',
|
|
kotlin: 'Kotlin',
|
|
dart: 'Dart',
|
|
liquid: 'Liquid',
|
|
unknown: 'Unknown',
|
|
};
|
|
return names[language] || language;
|
|
}
|