From b795bd0bfd24a2e4151c219761ffebaaa47c6fe1 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 8 May 2026 01:12:55 +0300 Subject: [PATCH 1/5] Add regex-based fallback parser for non-tree-sitter languages Tree-sitter grammars are currently only registered for TypeScript/JavaScript, so code_search returns nothing for the ~40 other extensions already mapped in EXT_TO_LANGUAGE. This change adds a regex-based fallback that runs when no tree-sitter grammar is registered for the detected language, plus built-in patterns for ~16 common languages including Python, Go, Rust, Ruby, Java, Kotlin, C/C++, C#, Swift, PHP, Lua, GDScript, GLSL, Dart, Shell, SQL, Scala, Elixir, Haskell. The fallback is best-effort: line-anchored regex extracts function/class/ struct/enum definitions and import statements. Less accurate than true AST parsing, but enough to make code_search surface results. Also adds extension mappings for Godot (.gd, .gdshader, .gdshaderinc) and common shader files (.glsl, .vert, .frag, .geom, .tesc, .tese, .comp, .hlsl). --- src/graphs/file-lang.ts | 15 ++ src/lib/parsers/code.ts | 99 ++++--- src/lib/parsers/languages/index.ts | 30 ++- src/lib/parsers/languages/regex-mapper.ts | 168 ++++++++++++ src/lib/parsers/languages/regex-patterns.ts | 274 ++++++++++++++++++++ src/lib/parsers/languages/registry.ts | 41 ++- src/lib/parsers/languages/types.ts | 14 +- src/tests/regex-parser.test.ts | 191 ++++++++++++++ 8 files changed, 786 insertions(+), 46 deletions(-) create mode 100644 src/lib/parsers/languages/regex-mapper.ts create mode 100644 src/lib/parsers/languages/regex-patterns.ts create mode 100644 src/tests/regex-parser.test.ts diff --git a/src/graphs/file-lang.ts b/src/graphs/file-lang.ts index 84d003c..c2c7558 100644 --- a/src/graphs/file-lang.ts +++ b/src/graphs/file-lang.ts @@ -123,6 +123,21 @@ export const EXT_TO_LANGUAGE: Record = { // Zig '.zig': 'zig', + + // Godot + '.gd': 'gdscript', + '.gdshader': 'glsl', + '.gdshaderinc': 'glsl', + + // Shaders + '.glsl': 'glsl', + '.vert': 'glsl', + '.frag': 'glsl', + '.geom': 'glsl', + '.tesc': 'glsl', + '.tese': 'glsl', + '.comp': 'glsl', + '.hlsl': 'glsl', }; /** Look up language from file extension. Returns null if unknown. */ diff --git a/src/lib/parsers/code.ts b/src/lib/parsers/code.ts index 37f6ffe..ece6590 100644 --- a/src/lib/parsers/code.ts +++ b/src/lib/parsers/code.ts @@ -1,7 +1,18 @@ import fs from 'fs'; import path from 'path'; import type { CodeNodeAttributes, CodeEdgeAttributes } from '@/graphs/code-types'; -import { parseSource, getMapper, isLanguageSupported } from '@/lib/parsers/languages'; +import { + parseSource, + getMapper, + getRegexMapper, + isLanguageSupported, + isRegexLanguageSupported, +} from '@/lib/parsers/languages'; +import type { + ExtractedSymbol, + ExtractedEdge, + ExtractedImport, +} from '@/lib/parsers/languages'; import { getLanguage } from '@/graphs/file-lang'; // Strip line and block comments from JSONC, preserving string contents. @@ -172,6 +183,18 @@ function resolveAliasImport(specifier: string, fromFile: string, projectDir: str // Main parser // --------------------------------------------------------------------------- +function makeFileOnlyResult(fileId: string, mtime: number): ParsedFile { + return { + fileId, + mtime, + nodes: [{ + id: fileId, + attrs: makeFileAttrs(fileId, '', '', 1, mtime), + }], + edges: [], + }; +} + export async function parseCodeFile( absolutePath: string, codeDir: string, @@ -183,46 +206,48 @@ export async function parseCodeFile( const ext = path.extname(absolutePath); const language = getLanguage(ext); - if (!language || !isLanguageSupported(language)) { - // Unsupported language — return file-only node, no symbols - return { - fileId, - mtime, - nodes: [{ - id: fileId, - attrs: makeFileAttrs(fileId, '', '', 1, mtime), - }], - edges: [], - }; + if (!language) return makeFileOnlyResult(fileId, mtime); + + const treeSitterAvailable = isLanguageSupported(language); + const regexAvailable = !treeSitterAvailable && isRegexLanguageSupported(language); + + if (!treeSitterAvailable && !regexAvailable) { + // Language is detected but no parser available — return file-only node. + return makeFileOnlyResult(fileId, mtime); } const source = fs.readFileSync(absolutePath, 'utf-8'); - const tree = await parseSource(source, language); - - if (!tree) { - return { - fileId, - mtime, - nodes: [{ - id: fileId, - attrs: makeFileAttrs(fileId, '', '', 1, mtime), - }], - edges: [], - }; - } - const rootNode = tree.rootNode; - const mapper = getMapper(language)!; - let symbols, edgeInfos, imports, fileDocComment, importSummary, lastLine; - try { - symbols = mapper.extractSymbols(rootNode); - edgeInfos = mapper.extractEdges(rootNode); - imports = mapper.extractImports(rootNode); - fileDocComment = extractFileDocComment(rootNode); - importSummary = buildImportSummary(rootNode); - lastLine = (rootNode.endPosition?.row ?? 0) + 1; - } finally { - tree.delete(); + let symbols: ExtractedSymbol[]; + let edgeInfos: ExtractedEdge[]; + let imports: ExtractedImport[]; + let fileDocComment = ''; + let importSummary = ''; + let lastLine: number; + + if (treeSitterAvailable) { + const tree = await parseSource(source, language); + if (!tree) return makeFileOnlyResult(fileId, mtime); + + const rootNode = tree.rootNode; + const mapper = getMapper(language)!; + try { + symbols = mapper.extractSymbols(rootNode); + edgeInfos = mapper.extractEdges(rootNode); + imports = mapper.extractImports(rootNode); + fileDocComment = extractFileDocComment(rootNode); + importSummary = buildImportSummary(rootNode); + lastLine = (rootNode.endPosition?.row ?? 0) + 1; + } finally { + tree.delete(); + } + } else { + // Regex fallback path — operates on raw source text. + const mapper = getRegexMapper(language)!; + symbols = mapper.extractSymbols(source); + edgeInfos = mapper.extractEdges(source); + imports = mapper.extractImports(source); + lastLine = source.split(/\r?\n/).length; } const nodes: ParsedFile['nodes'] = []; diff --git a/src/lib/parsers/languages/index.ts b/src/lib/parsers/languages/index.ts index ac03474..476f269 100644 --- a/src/lib/parsers/languages/index.ts +++ b/src/lib/parsers/languages/index.ts @@ -1,7 +1,33 @@ -export { registerLanguage, isLanguageSupported, parseSource, getMapper, listLanguages, initParser } from './registry'; -export type { LanguageMapper, ExtractedSymbol, ExtractedEdge, ExtractedImport } from './types'; +export { + registerLanguage, + registerRegexLanguage, + isLanguageSupported, + isRegexLanguageSupported, + parseSource, + getMapper, + getRegexMapper, + listLanguages, + listRegexLanguages, + initParser, +} from './registry'; +export type { + LanguageMapper, + RegexLanguageMapper, + ExtractedSymbol, + ExtractedEdge, + ExtractedImport, +} from './types'; export { registerTypescript } from './typescript'; +export { + createRegexMapper, + type RegexMapperOptions, + type RegexSymbolPattern, + type RegexImportPattern, +} from './regex-mapper'; +export { registerRegexLanguages } from './regex-patterns'; // Auto-register built-in languages on import import { registerTypescript } from './typescript'; +import { registerRegexLanguages } from './regex-patterns'; registerTypescript(); +registerRegexLanguages(); diff --git a/src/lib/parsers/languages/regex-mapper.ts b/src/lib/parsers/languages/regex-mapper.ts new file mode 100644 index 0000000..61f5ed5 --- /dev/null +++ b/src/lib/parsers/languages/regex-mapper.ts @@ -0,0 +1,168 @@ +/** + * Regex-based language parsing — fallback when no tree-sitter grammar is + * available. Operates directly on source text using line-anchored patterns + * to extract function/class/etc. definitions. Less accurate than tree-sitter, + * but works for any text-based language without external grammar dependencies. + */ +import type { CodeNodeKind } from '@/graphs/code-types'; +import type { + ExtractedSymbol, + ExtractedEdge, + ExtractedImport, + RegexLanguageMapper, +} from './types'; + +const SIGNATURE_MAX_LEN = 200; + +/** A regex pattern that recognizes a category of symbol. */ +export interface RegexSymbolPattern { + /** Symbol kind to assign when this pattern matches. */ + kind: CodeNodeKind; + /** Regex with a named group `name`. The mapper auto-applies the `gm` flags. */ + pattern: RegExp; +} + +/** A regex pattern for import/include statements. */ +export interface RegexImportPattern { + /** Regex with a named group `specifier`. The mapper auto-applies the `gm` flags. */ + pattern: RegExp; +} + +export interface RegexMapperOptions { + symbols: RegexSymbolPattern[]; + imports?: RegexImportPattern[]; + /** + * Pattern matching a single doc-comment line (e.g. /^\s*#/ for shell-style, + * /^\s*\/\// for C-style). When set, contiguous comment lines preceding a + * symbol are attached as its docComment. + */ + docCommentLine?: RegExp; +} + +function truncate(text: string, maxLen = SIGNATURE_MAX_LEN): string { + const collapsed = text.replace(/\s+/g, ' ').trim(); + return collapsed.length > maxLen ? collapsed.slice(0, maxLen) + '…' : collapsed; +} + +/** Ensure a regex has each of the required flags. */ +function withFlags(re: RegExp, required: string): RegExp { + let flags = re.flags; + for (const f of required) if (!flags.includes(f)) flags += f; + return new RegExp(re.source, flags); +} + +/** Convert a 0-based byte offset into a 1-based line number. */ +function offsetToLine(source: string, offset: number): number { + let line = 1; + const limit = Math.min(offset, source.length); + for (let i = 0; i < limit; i++) { + if (source.charCodeAt(i) === 10 /* \n */) line++; + } + return line; +} + +/** Walk backward from `beforeLineIdx-1`, collecting contiguous comment lines. */ +function collectDocComment(lines: string[], beforeLineIdx: number, pattern: RegExp): string { + const collected: string[] = []; + for (let i = beforeLineIdx - 1; i >= 0; i--) { + const line = lines[i]; + if (line === undefined) break; + if (pattern.test(line)) { + collected.unshift(line.trim()); + } else { + break; + } + } + return collected.join('\n'); +} + +/** Drop duplicates with the same name+startLine (multiple patterns can match the same span). */ +function dedupeSymbols(symbols: ExtractedSymbol[]): ExtractedSymbol[] { + const seen = new Set(); + const out: ExtractedSymbol[] = []; + for (const s of symbols) { + const key = `${s.name}:${s.startLine}`; + if (seen.has(key)) continue; + seen.add(key); + out.push(s); + } + return out; +} + +/** Build a RegexLanguageMapper from a set of patterns. */ +export function createRegexMapper(opts: RegexMapperOptions): RegexLanguageMapper { + const symbolPatterns = opts.symbols.map(p => ({ + ...p, + pattern: withFlags(p.pattern, 'gm'), + })); + const importPatterns = (opts.imports ?? []).map(p => ({ + ...p, + pattern: withFlags(p.pattern, 'gm'), + })); + const docCommentLine = opts.docCommentLine; + + return { + extractSymbols(source: string): ExtractedSymbol[] { + const symbols: ExtractedSymbol[] = []; + const lines = source.split(/\r?\n/); + + for (const { kind, pattern } of symbolPatterns) { + pattern.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = pattern.exec(source)) !== null) { + if (m[0].length === 0) { + pattern.lastIndex++; + continue; + } + const name = m.groups?.name; + if (!name) continue; + const startLine = offsetToLine(source, m.index); + const endLine = startLine + m[0].split(/\r?\n/).length - 1; + const signature = truncate(lines[startLine - 1] ?? m[0]); + const docComment = docCommentLine + ? collectDocComment(lines, startLine - 1, docCommentLine) + : ''; + symbols.push({ + name, + kind, + signature, + docComment, + body: m[0], + startLine, + endLine, + // Regex parsing has no scope info — assume top-level definitions + // are the public API. + isExported: true, + }); + } + } + + symbols.sort((a, b) => a.startLine - b.startLine || a.name.localeCompare(b.name)); + return dedupeSymbols(symbols); + }, + + extractEdges(_source: string): ExtractedEdge[] { + // Inheritance edges (extends/implements) are not extracted via regex. + // Recovering them robustly across syntaxes (`extends Foo`, `: public Foo`, + // `<: Foo`, `impl Foo for Bar`, …) is too noisy without an AST. + return []; + }, + + extractImports(source: string): ExtractedImport[] { + const out: ExtractedImport[] = []; + for (const { pattern } of importPatterns) { + pattern.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = pattern.exec(source)) !== null) { + if (m[0].length === 0) { + pattern.lastIndex++; + continue; + } + const specifier = m.groups?.specifier; + if (specifier) out.push({ specifier }); + } + } + return out; + }, + }; +} diff --git a/src/lib/parsers/languages/regex-patterns.ts b/src/lib/parsers/languages/regex-patterns.ts new file mode 100644 index 0000000..df324fc --- /dev/null +++ b/src/lib/parsers/languages/regex-patterns.ts @@ -0,0 +1,274 @@ +/** + * Built-in regex fallback patterns for languages without tree-sitter support. + * Patterns are best-effort — they don't model every edge case, but they + * extract enough symbols for code search to be useful. + */ +import { createRegexMapper } from './regex-mapper'; +import { registerRegexLanguage } from './registry'; + +const HASH_LINE = /^\s*#/; +const SLASH_LINE = /^\s*\/\//; +const DASH_LINE = /^\s*--/; + +let _registered = false; + +/** Register the built-in regex fallback mappers. Idempotent. */ +export function registerRegexLanguages(): void { + if (_registered) return; + _registered = true; + + // ---- Python ---- + registerRegexLanguage('python', createRegexMapper({ + docCommentLine: HASH_LINE, + symbols: [ + { kind: 'function', pattern: /^[ \t]*(?:async\s+)?def\s+(?[A-Za-z_]\w*)\s*\(/m }, + { kind: 'class', pattern: /^[ \t]*class\s+(?[A-Za-z_]\w*)\b/m }, + ], + imports: [ + { pattern: /^\s*from\s+(?[\w.]+)\s+import\b/m }, + { pattern: /^\s*import\s+(?[\w.]+)/m }, + ], + })); + + // ---- Go ---- + registerRegexLanguage('go', createRegexMapper({ + docCommentLine: SLASH_LINE, + symbols: [ + { kind: 'function', pattern: /^func\s+(?:\([^)]*\)\s+)?(?[A-Za-z_]\w*)\s*\(/m }, + { kind: 'class', pattern: /^type\s+(?[A-Za-z_]\w*)\s+struct\b/m }, + { kind: 'interface', pattern: /^type\s+(?[A-Za-z_]\w*)\s+interface\b/m }, + { kind: 'type', pattern: /^type\s+(?[A-Za-z_]\w*)\s+[A-Za-z_]/m }, + ], + imports: [ + { pattern: /^\s*import\s+"(?[^"]+)"/m }, + ], + })); + + // ---- Rust ---- + registerRegexLanguage('rust', createRegexMapper({ + docCommentLine: SLASH_LINE, + symbols: [ + { kind: 'function', pattern: /^[ \t]*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(?:const\s+)?fn\s+(?[A-Za-z_]\w*)/m }, + { kind: 'class', pattern: /^[ \t]*(?:pub(?:\([^)]*\))?\s+)?struct\s+(?[A-Za-z_]\w*)/m }, + { kind: 'enum', pattern: /^[ \t]*(?:pub(?:\([^)]*\))?\s+)?enum\s+(?[A-Za-z_]\w*)/m }, + { kind: 'interface', pattern: /^[ \t]*(?:pub(?:\([^)]*\))?\s+)?trait\s+(?[A-Za-z_]\w*)/m }, + { kind: 'type', pattern: /^[ \t]*(?:pub(?:\([^)]*\))?\s+)?type\s+(?[A-Za-z_]\w*)/m }, + ], + imports: [ + { pattern: /^\s*use\s+(?[\w:]+)/m }, + ], + })); + + // ---- Ruby ---- + registerRegexLanguage('ruby', createRegexMapper({ + docCommentLine: HASH_LINE, + symbols: [ + { kind: 'function', pattern: /^[ \t]*def\s+(?:self\.)?(?[A-Za-z_]\w*[!?=]?)/m }, + { kind: 'class', pattern: /^[ \t]*class\s+(?[A-Z]\w*)/m }, + { kind: 'interface', pattern: /^[ \t]*module\s+(?[A-Z]\w*)/m }, + ], + imports: [ + { pattern: /^\s*require\s+['"](?[^'"]+)['"]/m }, + { pattern: /^\s*require_relative\s+['"](?[^'"]+)['"]/m }, + ], + })); + + // ---- Java ---- + registerRegexLanguage('java', createRegexMapper({ + docCommentLine: SLASH_LINE, + symbols: [ + { kind: 'class', pattern: /^[ \t]*(?:public\s+|protected\s+|private\s+|abstract\s+|final\s+|static\s+)*class\s+(?[A-Za-z_]\w*)/m }, + { kind: 'interface', pattern: /^[ \t]*(?:public\s+|protected\s+|private\s+)*interface\s+(?[A-Za-z_]\w*)/m }, + { kind: 'enum', pattern: /^[ \t]*(?:public\s+|protected\s+|private\s+)*enum\s+(?[A-Za-z_]\w*)/m }, + ], + imports: [ + { pattern: /^\s*import\s+(?:static\s+)?(?[\w.]+)\s*;/m }, + ], + })); + + // ---- Kotlin ---- + registerRegexLanguage('kotlin', createRegexMapper({ + docCommentLine: SLASH_LINE, + symbols: [ + { kind: 'function', pattern: /^[ \t]*(?:public\s+|private\s+|internal\s+|protected\s+|inline\s+|suspend\s+|override\s+|open\s+|operator\s+|infix\s+)*fun\s+(?:<[^>]+>\s+)?(?:[\w.]+\.)?(?[A-Za-z_]\w*)/m }, + { kind: 'class', pattern: /^[ \t]*(?:public\s+|private\s+|internal\s+|protected\s+|abstract\s+|open\s+|sealed\s+|data\s+|inner\s+|annotation\s+)*class\s+(?[A-Za-z_]\w*)/m }, + { kind: 'interface', pattern: /^[ \t]*(?:public\s+|private\s+|internal\s+|protected\s+)*interface\s+(?[A-Za-z_]\w*)/m }, + { kind: 'class', pattern: /^[ \t]*(?:public\s+|private\s+|internal\s+)*object\s+(?[A-Za-z_]\w*)/m }, + ], + imports: [ + { pattern: /^\s*import\s+(?[\w.]+)/m }, + ], + })); + + // ---- C / C++ ---- + const cMapper = createRegexMapper({ + docCommentLine: SLASH_LINE, + symbols: [ + { kind: 'class', pattern: /^[ \t]*(?:typedef\s+)?struct\s+(?[A-Za-z_]\w*)\s*\{/m }, + { kind: 'class', pattern: /^[ \t]*class\s+(?[A-Za-z_]\w*)\b/m }, + { kind: 'enum', pattern: /^[ \t]*(?:typedef\s+)?enum\s+(?:class\s+)?(?[A-Za-z_]\w*)\b/m }, + ], + imports: [ + { pattern: /^\s*#\s*include\s*[<"](?[^>"]+)[>"]/m }, + ], + }); + registerRegexLanguage('c', cMapper); + registerRegexLanguage('cpp', cMapper); + + // ---- C# ---- + registerRegexLanguage('csharp', createRegexMapper({ + docCommentLine: SLASH_LINE, + symbols: [ + { kind: 'class', pattern: /^[ \t]*(?:public\s+|private\s+|internal\s+|protected\s+|abstract\s+|sealed\s+|static\s+|partial\s+)*class\s+(?[A-Za-z_]\w*)/m }, + { kind: 'interface', pattern: /^[ \t]*(?:public\s+|private\s+|internal\s+|protected\s+)*interface\s+(?I[A-Za-z_]\w*)/m }, + { kind: 'enum', pattern: /^[ \t]*(?:public\s+|private\s+|internal\s+)*enum\s+(?[A-Za-z_]\w*)/m }, + { kind: 'type', pattern: /^[ \t]*(?:public\s+|private\s+|internal\s+)*struct\s+(?[A-Za-z_]\w*)/m }, + ], + imports: [ + { pattern: /^\s*using\s+(?[\w.]+)\s*;/m }, + ], + })); + + // ---- Swift ---- + registerRegexLanguage('swift', createRegexMapper({ + docCommentLine: SLASH_LINE, + symbols: [ + { kind: 'function', pattern: /^[ \t]*(?:public\s+|private\s+|internal\s+|fileprivate\s+|open\s+|static\s+|final\s+|override\s+|mutating\s+)*func\s+(?[A-Za-z_]\w*)/m }, + { kind: 'class', pattern: /^[ \t]*(?:public\s+|private\s+|internal\s+|open\s+|final\s+)*class\s+(?[A-Za-z_]\w*)/m }, + { kind: 'class', pattern: /^[ \t]*(?:public\s+|private\s+|internal\s+|open\s+)*struct\s+(?[A-Za-z_]\w*)/m }, + { kind: 'interface', pattern: /^[ \t]*(?:public\s+|private\s+|internal\s+|open\s+)*protocol\s+(?[A-Za-z_]\w*)/m }, + { kind: 'enum', pattern: /^[ \t]*(?:public\s+|private\s+|internal\s+|open\s+)*enum\s+(?[A-Za-z_]\w*)/m }, + ], + imports: [ + { pattern: /^\s*import\s+(?[\w.]+)/m }, + ], + })); + + // ---- PHP ---- + registerRegexLanguage('php', createRegexMapper({ + docCommentLine: SLASH_LINE, + symbols: [ + { kind: 'function', pattern: /^[ \t]*(?:public\s+|private\s+|protected\s+|static\s+|abstract\s+|final\s+)*function\s+(?[A-Za-z_]\w*)/m }, + { kind: 'class', pattern: /^[ \t]*(?:abstract\s+|final\s+)*class\s+(?[A-Za-z_]\w*)/m }, + { kind: 'interface', pattern: /^[ \t]*interface\s+(?[A-Za-z_]\w*)/m }, + ], + imports: [ + { pattern: /^\s*use\s+(?[\w\\]+)/m }, + ], + })); + + // ---- Lua ---- + registerRegexLanguage('lua', createRegexMapper({ + docCommentLine: DASH_LINE, + symbols: [ + { kind: 'function', pattern: /^[ \t]*(?:local\s+)?function\s+(?:[\w.:]+[.:])?(?[A-Za-z_]\w*)/m }, + { kind: 'function', pattern: /^[ \t]*(?:local\s+)?(?[A-Za-z_]\w*)\s*=\s*function/m }, + ], + imports: [ + { pattern: /\brequire\s*\(?\s*['"](?[^'"]+)['"]/m }, + ], + })); + + // ---- GDScript (Godot) ---- + registerRegexLanguage('gdscript', createRegexMapper({ + docCommentLine: HASH_LINE, + symbols: [ + { kind: 'function', pattern: /^[ \t]*(?:static\s+)?func\s+(?_?[A-Za-z_]\w*)\s*\(/m }, + { kind: 'class', pattern: /^[ \t]*class\s+(?[A-Za-z_]\w*)/m }, + { kind: 'class', pattern: /^[ \t]*class_name\s+(?[A-Za-z_]\w*)/m }, + { kind: 'enum', pattern: /^[ \t]*enum\s+(?[A-Za-z_]\w*)/m }, + { kind: 'variable', pattern: /^[ \t]*signal\s+(?[A-Za-z_]\w*)/m }, + { kind: 'variable', pattern: /^[ \t]*@export(?:\([^)]*\))?\s+(?:var|onready\s+var)\s+(?[A-Za-z_]\w*)/m }, + ], + imports: [ + { pattern: /\b(?:preload|load)\s*\(\s*['"](?[^'"]+)['"]/m }, + ], + })); + + // ---- GLSL / shader ---- + const glslMapper = createRegexMapper({ + docCommentLine: SLASH_LINE, + symbols: [ + { kind: 'function', pattern: /^[A-Za-z_][\w]*\s+(?[A-Za-z_]\w*)\s*\([^)]*\)\s*\{/m }, + { kind: 'variable', pattern: /^\s*uniform\s+[\w\s]+?(?[A-Za-z_]\w*)\s*[;=]/m }, + { kind: 'type', pattern: /^[ \t]*struct\s+(?[A-Za-z_]\w*)\s*\{/m }, + ], + imports: [ + { pattern: /^\s*#\s*include\s*[<"](?[^>"]+)[>"]/m }, + ], + }); + registerRegexLanguage('glsl', glslMapper); + + // ---- Dart ---- + registerRegexLanguage('dart', createRegexMapper({ + docCommentLine: SLASH_LINE, + symbols: [ + { kind: 'class', pattern: /^[ \t]*(?:abstract\s+)?class\s+(?[A-Za-z_]\w*)/m }, + { kind: 'interface', pattern: /^[ \t]*mixin\s+(?[A-Za-z_]\w*)/m }, + ], + imports: [ + { pattern: /^\s*import\s+['"](?[^'"]+)['"]/m }, + ], + })); + + // ---- Shell / Bash ---- + registerRegexLanguage('shell', createRegexMapper({ + docCommentLine: HASH_LINE, + symbols: [ + { kind: 'function', pattern: /^[ \t]*(?:function\s+)?(?[A-Za-z_]\w*)\s*\(\s*\)\s*\{/m }, + ], + imports: [ + { pattern: /^\s*(?:source|\.)\s+(?[^\s;]+)/m }, + ], + })); + + // ---- SQL ---- + registerRegexLanguage('sql', createRegexMapper({ + docCommentLine: DASH_LINE, + symbols: [ + { kind: 'function', pattern: /^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:FUNCTION|PROCEDURE)\s+(?[A-Za-z_][\w.]*)/im }, + { kind: 'type', pattern: /^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:TABLE|VIEW|TYPE)\s+(?:IF\s+NOT\s+EXISTS\s+)?(?[A-Za-z_][\w.]*)/im }, + ], + imports: [], + })); + + // ---- Scala ---- + registerRegexLanguage('scala', createRegexMapper({ + docCommentLine: SLASH_LINE, + symbols: [ + { kind: 'function', pattern: /^[ \t]*(?:override\s+|private\s+|protected\s+|public\s+|implicit\s+)*def\s+(?[A-Za-z_]\w*)/m }, + { kind: 'class', pattern: /^[ \t]*(?:abstract\s+|sealed\s+|final\s+|case\s+)*class\s+(?[A-Za-z_]\w*)/m }, + { kind: 'interface', pattern: /^[ \t]*(?:sealed\s+)?trait\s+(?[A-Za-z_]\w*)/m }, + { kind: 'class', pattern: /^[ \t]*(?:case\s+)?object\s+(?[A-Za-z_]\w*)/m }, + ], + imports: [ + { pattern: /^\s*import\s+(?[\w.]+)/m }, + ], + })); + + // ---- Elixir ---- + registerRegexLanguage('elixir', createRegexMapper({ + docCommentLine: HASH_LINE, + symbols: [ + { kind: 'function', pattern: /^[ \t]*def(?:p)?\s+(?[A-Za-z_]\w*)/m }, + { kind: 'class', pattern: /^[ \t]*defmodule\s+(?[A-Z][\w.]*)/m }, + ], + imports: [ + { pattern: /^\s*(?:import|alias|use|require)\s+(?[A-Z][\w.]*)/m }, + ], + })); + + // ---- Haskell ---- + registerRegexLanguage('haskell', createRegexMapper({ + docCommentLine: DASH_LINE, + symbols: [ + { kind: 'function', pattern: /^(?[a-z]\w*)\s*::/m }, + { kind: 'class', pattern: /^data\s+(?[A-Z]\w*)/m }, + { kind: 'type', pattern: /^type\s+(?[A-Z]\w*)/m }, + { kind: 'interface', pattern: /^class\s+(?:\([^)]+\)\s+=>\s+)?(?[A-Z]\w*)/m }, + ], + imports: [ + { pattern: /^\s*import\s+(?:qualified\s+)?(?[\w.]+)/m }, + ], + })); +} diff --git a/src/lib/parsers/languages/registry.ts b/src/lib/parsers/languages/registry.ts index 1c0b430..0ec7c0c 100644 --- a/src/lib/parsers/languages/registry.ts +++ b/src/lib/parsers/languages/registry.ts @@ -1,7 +1,13 @@ import path from 'path'; -import type { LanguageMapper } from './types'; +import type { LanguageMapper, RegexLanguageMapper } from './types'; -export { type LanguageMapper, type ExtractedSymbol, type ExtractedEdge, type ExtractedImport } from './types'; +export { + type LanguageMapper, + type RegexLanguageMapper, + type ExtractedSymbol, + type ExtractedEdge, + type ExtractedImport, +} from './types'; // web-tree-sitter types (loaded lazily) type WTSLanguage = any; @@ -18,6 +24,9 @@ interface LanguageEntry { /** Map from language name (matching file-lang.ts names) to entry. */ const languages = new Map(); +/** Map from language name to a regex-based fallback mapper. */ +const regexLanguages = new Map(); + /** WASM directory containing grammar .wasm files */ const WASM_DIR = path.join( path.dirname(require.resolve('@vscode/tree-sitter-wasm/package.json')), @@ -42,11 +51,16 @@ export async function initParser(): Promise { return _initPromise; } -/** Register a language (sync — only stores metadata). */ +/** Register a tree-sitter language (sync — only stores metadata). */ export function registerLanguage(name: string, wasmFile: string, mapper: LanguageMapper): void { languages.set(name, { wasmFile, language: null, mapper }); } +/** Register a regex-based fallback mapper for a language without tree-sitter support. */ +export function registerRegexLanguage(name: string, mapper: RegexLanguageMapper): void { + regexLanguages.set(name, mapper); +} + /** Load a language WASM if not already loaded. */ async function loadLanguage(entry: LanguageEntry): Promise { if (entry.language) return entry.language; @@ -56,11 +70,16 @@ async function loadLanguage(entry: LanguageEntry): Promise { return entry.language; } -/** Check if a language is registered. */ +/** Check if a tree-sitter language is registered. */ export function isLanguageSupported(languageName: string): boolean { return languages.has(languageName); } +/** Check if a regex-fallback mapper is registered for a language. */ +export function isRegexLanguageSupported(languageName: string): boolean { + return regexLanguages.has(languageName); +} + /** Reusable parser per language (avoids WASM memory leak from creating Parser on every call). */ const parsers = new Map(); @@ -81,12 +100,22 @@ export async function parseSource(code: string, languageName: string): Promise { + registerRegexLanguages(); +}); + +describe('createRegexMapper', () => { + const mapper = createRegexMapper({ + docCommentLine: /^\s*#/, + symbols: [ + { kind: 'function', pattern: /^def\s+(?\w+)\s*\(/m }, + { kind: 'class', pattern: /^class\s+(?\w+)/m }, + ], + imports: [ + { pattern: /^import\s+(?\S+)/m }, + ], + }); + + it('extracts function name', () => { + const out = mapper.extractSymbols('def hello(x):\n pass\n'); + expect(out.map(s => s.name)).toEqual(['hello']); + expect(out[0].kind).toBe('function'); + expect(out[0].startLine).toBe(1); + }); + + it('extracts class name', () => { + const out = mapper.extractSymbols('class Foo:\n pass\n'); + expect(out.map(s => s.name)).toEqual(['Foo']); + expect(out[0].kind).toBe('class'); + }); + + it('extracts both functions and classes from same source', () => { + const src = 'class Foo:\n pass\ndef bar():\n pass\n'; + const out = mapper.extractSymbols(src); + expect(out).toHaveLength(2); + expect(out.map(s => s.name).sort()).toEqual(['Foo', 'bar']); + }); + + it('attaches preceding comment lines as docComment', () => { + const src = '# helper for things\n# does X then Y\ndef hello():\n pass\n'; + const out = mapper.extractSymbols(src); + expect(out[0].docComment).toContain('helper for things'); + expect(out[0].docComment).toContain('does X then Y'); + }); + + it('extracts imports', () => { + const out = mapper.extractImports('import os\nimport sys.path\n'); + expect(out.map(i => i.specifier)).toEqual(['os', 'sys.path']); + }); + + it('returns empty edges array (regex parsing has no AST)', () => { + expect(mapper.extractEdges('class Foo extends Bar {}')).toEqual([]); + }); + + it('reports correct line numbers for multi-line source', () => { + const src = '\n\n\ndef hello():\n pass\n'; + const out = mapper.extractSymbols(src); + expect(out[0].startLine).toBe(4); + }); + + it('handles empty source', () => { + expect(mapper.extractSymbols('')).toEqual([]); + expect(mapper.extractImports('')).toEqual([]); + }); + + it('skips matches without a "name" group', () => { + const noNameMapper = createRegexMapper({ + symbols: [{ kind: 'function', pattern: /^def\s+\w+/m }], + }); + expect(noNameMapper.extractSymbols('def foo():\n')).toEqual([]); + }); + + it('marks symbols as exported (regex has no scope info)', () => { + const out = mapper.extractSymbols('def hello():\n pass\n'); + expect(out[0].isExported).toBe(true); + }); +}); + +describe('built-in regex languages', () => { + it('python is registered', () => { + expect(isRegexLanguageSupported('python')).toBe(true); + }); + + it('go is registered', () => { + expect(isRegexLanguageSupported('go')).toBe(true); + }); + + it('rust is registered', () => { + expect(isRegexLanguageSupported('rust')).toBe(true); + }); + + it('gdscript is registered', () => { + expect(isRegexLanguageSupported('gdscript')).toBe(true); + }); + + it('glsl is registered', () => { + expect(isRegexLanguageSupported('glsl')).toBe(true); + }); + + it('typescript is NOT regex-registered (handled by tree-sitter)', () => { + expect(isRegexLanguageSupported('typescript')).toBe(false); + }); + + describe('python mapper', () => { + const py = getRegexMapper('python')!; + it('extracts def', () => { + expect(py.extractSymbols('def foo():\n pass\n').map(s => s.name)).toEqual(['foo']); + }); + it('extracts async def', () => { + expect(py.extractSymbols('async def foo():\n pass\n').map(s => s.name)).toEqual(['foo']); + }); + it('extracts class', () => { + expect(py.extractSymbols('class Foo(Base):\n pass\n').map(s => s.name)).toEqual(['Foo']); + }); + it('extracts from … import', () => { + expect(py.extractImports('from os.path import join\n').map(i => i.specifier)).toEqual(['os.path']); + }); + it('extracts plain import', () => { + expect(py.extractImports('import sys\n').map(i => i.specifier)).toEqual(['sys']); + }); + }); + + describe('go mapper', () => { + const go = getRegexMapper('go')!; + it('extracts func', () => { + expect(go.extractSymbols('func Hello() {}\n').map(s => s.name)).toEqual(['Hello']); + }); + it('extracts method receiver func', () => { + expect(go.extractSymbols('func (s *Server) Run() {}\n').map(s => s.name)).toEqual(['Run']); + }); + it('extracts struct type', () => { + const out = go.extractSymbols('type Server struct {}\n'); + expect(out.map(s => s.name)).toEqual(['Server']); + expect(out[0].kind).toBe('class'); + }); + it('extracts import', () => { + expect(go.extractImports('import "fmt"\n').map(i => i.specifier)).toEqual(['fmt']); + }); + }); + + describe('rust mapper', () => { + const rs = getRegexMapper('rust')!; + it('extracts pub fn', () => { + expect(rs.extractSymbols('pub fn launch() {}\n').map(s => s.name)).toEqual(['launch']); + }); + it('extracts struct', () => { + expect(rs.extractSymbols('pub struct Engine {}\n').map(s => s.name)).toEqual(['Engine']); + }); + it('extracts trait', () => { + const out = rs.extractSymbols('pub trait Runnable {}\n'); + expect(out[0].kind).toBe('interface'); + }); + }); + + describe('gdscript mapper', () => { + const gd = getRegexMapper('gdscript')!; + it('extracts func', () => { + const src = 'func _ready():\n\tpass\nfunc fire(target):\n\tpass\n'; + expect(gd.extractSymbols(src).map(s => s.name)).toEqual(['_ready', 'fire']); + }); + it('extracts class_name', () => { + const out = gd.extractSymbols('class_name Player\nextends Node\n'); + expect(out.map(s => s.name)).toContain('Player'); + }); + it('extracts signal', () => { + const out = gd.extractSymbols('signal hit_target\n'); + expect(out.map(s => s.name)).toEqual(['hit_target']); + }); + it('extracts preload import', () => { + const out = gd.extractImports('var Bullet = preload("res://scenes/bullet.tscn")\n'); + expect(out.map(i => i.specifier)).toEqual(['res://scenes/bullet.tscn']); + }); + }); + + describe('glsl mapper', () => { + const glsl = getRegexMapper('glsl')!; + it('extracts uniform', () => { + const out = glsl.extractSymbols('uniform float bass_impact;\n'); + expect(out.map(s => s.name)).toContain('bass_impact'); + }); + it('extracts function', () => { + const out = glsl.extractSymbols('vec3 fade(vec3 c, float t) {\n return c * t;\n}\n'); + expect(out.map(s => s.name)).toContain('fade'); + }); + }); +}); From 78bea55394356c44e8cebbb789224df59f520b94 Mon Sep 17 00:00:00 2001 From: 2dmaster Date: Tue, 12 May 2026 14:46:05 +0300 Subject: [PATCH 2/5] Add tree-sitter mappers for 10 languages + full Godot file support - Tree-sitter AST mappers: Python, Go, Rust, Java, PHP, Ruby, C#, C/C++, Bash, GDScript - Regex parsers for Godot scene/resource/project/extension file types - GDScript WASM committed to wasm/ with postinstall.js copying it into node_modules/@vscode/tree-sitter-wasm/wasm after npm install - Added .glsl to default codeInclude for Godot 4 external shader support - Ignored .mcp.json and .notes/ in .gitignore Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 2 + package-lock.json | 53 ++++ package.json | 7 +- scripts/postinstall.js | 22 ++ src/graphs/file-lang.ts | 5 + src/lib/multi-config.ts | 2 +- src/lib/parsers/languages/bash.ts | 86 ++++++ src/lib/parsers/languages/cpp.ts | 258 ++++++++++++++++ src/lib/parsers/languages/csharp.ts | 298 +++++++++++++++++++ src/lib/parsers/languages/gdscript.ts | 307 ++++++++++++++++++++ src/lib/parsers/languages/go.ts | 156 ++++++++++ src/lib/parsers/languages/helpers.ts | 90 ++++++ src/lib/parsers/languages/index.ts | 31 ++ src/lib/parsers/languages/java.ts | 234 +++++++++++++++ src/lib/parsers/languages/php.ts | 196 +++++++++++++ src/lib/parsers/languages/python.ts | 193 ++++++++++++ src/lib/parsers/languages/regex-patterns.ts | 296 ++++++++++--------- src/lib/parsers/languages/ruby.ts | 191 ++++++++++++ src/lib/parsers/languages/rust.ts | 218 ++++++++++++++ wasm/tree-sitter-gdscript.wasm | Bin 0 -> 292506 bytes 20 files changed, 2501 insertions(+), 144 deletions(-) create mode 100644 scripts/postinstall.js create mode 100644 src/lib/parsers/languages/bash.ts create mode 100644 src/lib/parsers/languages/cpp.ts create mode 100644 src/lib/parsers/languages/csharp.ts create mode 100644 src/lib/parsers/languages/gdscript.ts create mode 100644 src/lib/parsers/languages/go.ts create mode 100644 src/lib/parsers/languages/helpers.ts create mode 100644 src/lib/parsers/languages/java.ts create mode 100644 src/lib/parsers/languages/php.ts create mode 100644 src/lib/parsers/languages/python.ts create mode 100644 src/lib/parsers/languages/ruby.ts create mode 100644 src/lib/parsers/languages/rust.ts create mode 100755 wasm/tree-sitter-gdscript.wasm diff --git a/.gitignore b/.gitignore index 53e3ade..a002eaa 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ models/ site/node_modules site/.docusaurus site/build +.mcp.json +.notes/ diff --git a/package-lock.json b/package-lock.json index d6dd311..17fe3db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "pino": "^10.3.1", "pino-pretty": "^13.1.3", "redis": "^5.11.0", + "tree-sitter": "^0.21.1", "web-tree-sitter": "^0.26.7", "ws": "^8.19.0", "yaml": "^2.8.2", @@ -50,6 +51,7 @@ "graphology-types": "^0.24.8", "jest": "^30.3.0", "supertest": "^7.2.2", + "tree-sitter-gdscript": "^6.1.0", "ts-jest": "^29.4.6", "tsc-alias": "^1.8.16", "tsx": "^4.21.0", @@ -6413,6 +6415,26 @@ "dev": true, "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", + "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -8007,6 +8029,37 @@ "node": ">=0.6" } }, + "node_modules/tree-sitter": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz", + "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.0" + } + }, + "node_modules/tree-sitter-gdscript": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/tree-sitter-gdscript/-/tree-sitter-gdscript-6.1.0.tgz", + "integrity": "sha512-Uy5+GWLkec2JaS1mamiJYsC9j/JoW2FxFq4bnji95gyTtMTsqO6+RSVIaBTU/vRGSNEi3PVDZzyV0j3B4RdntA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.1", + "node-gyp-build": "^4.8.4" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, "node_modules/ts-jest": { "version": "29.4.6", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", diff --git a/package.json b/package.json index c3bcd15..de316a2 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch", "site:dev": "cd site && npm start", "site:build": "cd site && npm run build", - "site:serve": "cd site && npm run serve" + "site:serve": "cd site && npm run serve", + "postinstall": "node scripts/postinstall.js" }, "repository": { "type": "git", @@ -44,6 +45,8 @@ }, "files": [ "dist/", + "wasm/", + "scripts/postinstall.js", "README.md" ], "publishConfig": { @@ -69,6 +72,7 @@ "pino": "^10.3.1", "pino-pretty": "^13.1.3", "redis": "^5.11.0", + "tree-sitter": "^0.21.1", "web-tree-sitter": "^0.26.7", "ws": "^8.19.0", "yaml": "^2.8.2", @@ -88,6 +92,7 @@ "graphology-types": "^0.24.8", "jest": "^30.3.0", "supertest": "^7.2.2", + "tree-sitter-gdscript": "^6.1.0", "ts-jest": "^29.4.6", "tsc-alias": "^1.8.16", "tsx": "^4.21.0", diff --git a/scripts/postinstall.js b/scripts/postinstall.js new file mode 100644 index 0000000..af15dbc --- /dev/null +++ b/scripts/postinstall.js @@ -0,0 +1,22 @@ +#!/usr/bin/env node +'use strict'; + +const path = require('path'); +const fs = require('fs'); + +const src = path.join(__dirname, '..', 'wasm', 'tree-sitter-gdscript.wasm'); + +let wasmDir; +try { + wasmDir = path.join(path.dirname(require.resolve('@vscode/tree-sitter-wasm/package.json')), 'wasm'); +} catch { + // @vscode/tree-sitter-wasm not installed yet — skip + process.exit(0); +} + +const dest = path.join(wasmDir, 'tree-sitter-gdscript.wasm'); + +if (!fs.existsSync(dest)) { + fs.copyFileSync(src, dest); + console.log('graphmemory: installed tree-sitter-gdscript.wasm'); +} diff --git a/src/graphs/file-lang.ts b/src/graphs/file-lang.ts index c2c7558..651b528 100644 --- a/src/graphs/file-lang.ts +++ b/src/graphs/file-lang.ts @@ -128,6 +128,11 @@ export const EXT_TO_LANGUAGE: Record = { '.gd': 'gdscript', '.gdshader': 'glsl', '.gdshaderinc': 'glsl', + '.tscn': 'godot-scene', + '.escn': 'godot-scene', + '.tres': 'godot-resource', + '.godot': 'godot-project', + '.gdextension': 'godot-extension', // Shaders '.glsl': 'glsl', diff --git a/src/lib/multi-config.ts b/src/lib/multi-config.ts index 0d7360c..73c5875 100644 --- a/src/lib/multi-config.ts +++ b/src/lib/multi-config.ts @@ -407,7 +407,7 @@ const SERVER_DEFAULTS: Omit & { embedding: EmbeddingC const PROJECT_DEFAULTS = { docsInclude: '**/*.md', - codeInclude: '**/*.{js,ts,jsx,tsx,mjs,mts,cjs,cts}', + codeInclude: '**/*.{js,ts,jsx,tsx,mjs,mts,cjs,cts,gd,gdshader,gdshaderinc,glsl,tscn,escn,tres,godot,gdextension}', chunkDepth: 4, }; diff --git a/src/lib/parsers/languages/bash.ts b/src/lib/parsers/languages/bash.ts new file mode 100644 index 0000000..370cd27 --- /dev/null +++ b/src/lib/parsers/languages/bash.ts @@ -0,0 +1,86 @@ +import type { ExtractedSymbol, ExtractedEdge, ExtractedImport, LanguageMapper } from './types'; +import { registerLanguage } from './registry'; +import { + type TSNode, + truncate, + startLine, + endLine, + sliceBeforeBody, + buildBody, + getPrecedingDoc, +} from './helpers'; + +function getDoc(node: TSNode): string { + return getPrecedingDoc(node, ['comment'], '#'); +} + +function buildSig(node: TSNode): string { + const body = node.childForFieldName('body'); + if (!body) return truncate(node.text ?? ''); + const header = sliceBeforeBody(node, body); + return truncate(header ?? (node.text ?? '').split('\n')[0]); +} + +const bashMapper: LanguageMapper = { + extractSymbols(rootNode: TSNode): ExtractedSymbol[] { + const symbols: ExtractedSymbol[] = []; + + function visit(node: TSNode): void { + if (node.type === 'function_definition') { + const nameNode = node.childForFieldName('name'); + const name = nameNode?.text ?? ''; + if (name) { + const doc = getDoc(node); + symbols.push({ + name, + kind: 'function', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: !name.startsWith('_'), + }); + return; // don't recurse into function body + } + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return symbols; + }, + + extractEdges(_rootNode: TSNode): ExtractedEdge[] { + return []; + }, + + extractImports(rootNode: TSNode): ExtractedImport[] { + const imports: ExtractedImport[] = []; + + function visit(node: TSNode): void { + // source ./foo or . ./foo + if (node.type === 'command') { + const nameNode = node.childForFieldName('name'); + if (nameNode?.text === 'source' || nameNode?.text === '.') { + const args = node.childForFieldName('argument'); + const arg = args ?? node.namedChildren?.find((c: TSNode) => c.type === 'word'); + if (arg) imports.push({ specifier: arg.text }); + } + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return imports; + }, +}; + +let _registered = false; + +export function registerBash(): void { + if (_registered) return; + _registered = true; + registerLanguage('shell', 'tree-sitter-bash.wasm', bashMapper); + registerLanguage('bash', 'tree-sitter-bash.wasm', bashMapper); +} diff --git a/src/lib/parsers/languages/cpp.ts b/src/lib/parsers/languages/cpp.ts new file mode 100644 index 0000000..82f7ca0 --- /dev/null +++ b/src/lib/parsers/languages/cpp.ts @@ -0,0 +1,258 @@ +import type { ExtractedSymbol, ExtractedEdge, ExtractedImport, LanguageMapper } from './types'; +import { registerLanguage } from './registry'; +import { + type TSNode, + truncate, + startLine, + endLine, + sliceBeforeBody, + buildBody, + getPrecedingDoc, +} from './helpers'; + +function getDoc(node: TSNode): string { + return getPrecedingDoc(node, ['comment'], '/**') || + getPrecedingDoc(node, ['comment'], '//'); +} + +function buildSig(node: TSNode): string { + const body = node.childForFieldName('body'); + if (!body) return truncate(node.text ?? ''); + const header = sliceBeforeBody(node, body); + return truncate(header ?? (node.text ?? '').split('\n')[0]); +} + +/** + * Extract the function name from a C/C++ function_definition. + * function_definition.declarator can be: + * function_declarator → declarator (identifier | qualified_identifier | destructor_name | ...) + * pointer_declarator → declarator (function_declarator → ...) + * reference_declarator → ... + */ +function getFunctionName(node: TSNode): string | null { + function walkDeclarator(d: TSNode): string | null { + if (!d) return null; + if (d.type === 'identifier' || d.type === 'field_identifier') return d.text; + if (d.type === 'qualified_identifier') { + // last part of A::B::C + const name = d.childForFieldName('name'); + return name?.text ?? d.text; + } + if (d.type === 'destructor_name') return d.text; + if (d.type === 'operator_name') return d.text; + if (d.type === 'function_declarator') { + return walkDeclarator(d.childForFieldName('declarator')); + } + if (d.type === 'pointer_declarator' || d.type === 'reference_declarator' || + d.type === 'abstract_pointer_declarator') { + return walkDeclarator(d.childForFieldName('declarator')); + } + return null; + } + + const decl = node.childForFieldName('declarator'); + return walkDeclarator(decl); +} + +function extractClassMembers(body: TSNode): ExtractedSymbol[] { + const children: ExtractedSymbol[] = []; + if (!body) return children; + + for (const member of body.namedChildren ?? []) { + if (member.type === 'function_definition') { + const name = getFunctionName(member); + if (!name) continue; + const doc = getDoc(member); + children.push({ + name, + kind: 'method', + signature: buildSig(member), + docComment: doc, + body: buildBody(member, doc), + startLine: startLine(member), + endLine: endLine(member), + isExported: false, + }); + } else if (member.type === 'declaration') { + // Field declaration inside class + const decl = member.childForFieldName('declarator'); + const name = decl ? getFunctionName(decl) ?? decl.text : null; + if (!name) continue; + const doc = getDoc(member); + children.push({ + name, + kind: 'variable', + signature: truncate(member.text ?? ''), + docComment: doc, + body: buildBody(member, doc), + startLine: startLine(member), + endLine: endLine(member), + isExported: false, + }); + } + } + return children; +} + +function processTopLevel(node: TSNode): ExtractedSymbol[] { + switch (node.type) { + case 'function_definition': { + const name = getFunctionName(node); + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'function', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + }]; + } + case 'class_specifier': + case 'struct_specifier': { + const nameNode = node.childForFieldName('name'); + const name = nameNode?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + const body = node.childForFieldName('body'); + const children = extractClassMembers(body); + return [{ + name, + kind: node.type === 'struct_specifier' ? 'type' : 'class', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + children: children.length > 0 ? children : undefined, + }]; + } + case 'namespace_definition': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + const body = node.childForFieldName('body'); + const inner: ExtractedSymbol[] = []; + for (const child of body?.namedChildren ?? []) { + inner.push(...processTopLevel(child)); + } + return [{ + name, + kind: 'interface', + signature: truncate(node.text?.split('\n')[0] ?? ''), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + children: inner.length > 0 ? inner : undefined, + }]; + } + case 'template_declaration': { + // template<...> wraps a function or class — unwrap and process + for (const child of node.namedChildren ?? []) { + const results = processTopLevel(child); + if (results.length > 0) return results; + } + return []; + } + case 'enum_specifier': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'enum', + signature: truncate(node.text?.split('\n')[0] ?? ''), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + }]; + } + default: + return []; + } +} + +const cppMapper: LanguageMapper = { + extractSymbols(rootNode: TSNode): ExtractedSymbol[] { + const symbols: ExtractedSymbol[] = []; + + function visit(node: TSNode): void { + const results = processTopLevel(node); + if (results.length > 0) { + symbols.push(...results); + return; + } + // Recurse into declaration nodes (e.g. typedef struct) + if (node.type === 'declaration' || node.type === 'type_definition') { + for (const child of node.namedChildren ?? []) { + symbols.push(...processTopLevel(child)); + } + return; + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return symbols; + }, + + extractEdges(rootNode: TSNode): ExtractedEdge[] { + const edges: ExtractedEdge[] = []; + + function visit(node: TSNode): void { + if (node.type === 'class_specifier') { + const className = node.childForFieldName('name')?.text; + if (className) { + const baseClause = node.childForFieldName('base_class_clause'); + if (baseClause) { + for (const base of baseClause.namedChildren ?? []) { + if (base.type === 'type_identifier' || base.type === 'qualified_identifier') { + edges.push({ fromName: className, toName: base.text, kind: 'extends' }); + } + } + } + } + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return edges; + }, + + extractImports(rootNode: TSNode): ExtractedImport[] { + const imports: ExtractedImport[] = []; + + function visit(node: TSNode): void { + if (node.type === 'preproc_include') { + const path = node.childForFieldName('path'); + if (path) { + const specifier = path.text.replace(/^["<]|[">]$/g, ''); + imports.push({ specifier }); + } + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return imports; + }, +}; + +let _registered = false; + +export function registerCpp(): void { + if (_registered) return; + _registered = true; + // cpp WASM handles both C and C++ (C++ is a superset of C) + registerLanguage('cpp', 'tree-sitter-cpp.wasm', cppMapper); + registerLanguage('c', 'tree-sitter-cpp.wasm', cppMapper); +} diff --git a/src/lib/parsers/languages/csharp.ts b/src/lib/parsers/languages/csharp.ts new file mode 100644 index 0000000..3b0c764 --- /dev/null +++ b/src/lib/parsers/languages/csharp.ts @@ -0,0 +1,298 @@ +import type { ExtractedSymbol, ExtractedEdge, ExtractedImport, LanguageMapper } from './types'; +import { registerLanguage } from './registry'; +import { + type TSNode, + truncate, + startLine, + endLine, + sliceBeforeBody, + buildBody, + getPrecedingDoc, +} from './helpers'; + +function getDoc(node: TSNode): string { + // C# uses /// single-line doc comments or /** */ block comments + return getPrecedingDoc(node, ['single_line_doc_comment'], '///') || + getPrecedingDoc(node, ['multiline_comment'], '/**') || + getPrecedingDoc(node, ['single_line_comment']); +} + +function buildSig(node: TSNode): string { + const body = node.childForFieldName('body'); + if (!body) return truncate(node.text ?? ''); + const header = sliceBeforeBody(node, body); + return truncate(header ?? (node.text ?? '').split('\n')[0]); +} + +function hasModifier(node: TSNode, mod: string): boolean { + for (const child of node.children ?? []) { + if (child.type === 'modifier' && child.text === mod) return true; + } + return false; +} + +function isPublic(node: TSNode): boolean { + return hasModifier(node, 'public'); +} + +function extractTypeMembers(body: TSNode): ExtractedSymbol[] { + const children: ExtractedSymbol[] = []; + if (!body) return children; + for (const member of body.namedChildren ?? []) { + switch (member.type) { + case 'method_declaration': { + const name = member.childForFieldName('name')?.text ?? ''; + if (!name) continue; + const doc = getDoc(member); + children.push({ + name, + kind: 'method', + signature: buildSig(member), + docComment: doc, + body: buildBody(member, doc), + startLine: startLine(member), + endLine: endLine(member), + isExported: isPublic(member), + }); + break; + } + case 'constructor_declaration': { + const name = member.childForFieldName('name')?.text ?? ''; + if (!name) continue; + const doc = getDoc(member); + children.push({ + name, + kind: 'constructor', + signature: buildSig(member), + docComment: doc, + body: buildBody(member, doc), + startLine: startLine(member), + endLine: endLine(member), + isExported: isPublic(member), + }); + break; + } + case 'field_declaration': { + // field_declaration has variable_declarator children + for (const decl of member.namedChildren ?? []) { + if (decl.type === 'variable_declarator') { + const name = decl.childForFieldName('name')?.text ?? ''; + if (!name) continue; + const doc = getDoc(member); + children.push({ + name, + kind: 'variable', + signature: truncate(member.text ?? ''), + docComment: doc, + body: buildBody(member, doc), + startLine: startLine(member), + endLine: endLine(member), + isExported: isPublic(member), + }); + } + } + break; + } + case 'property_declaration': { + const name = member.childForFieldName('name')?.text ?? ''; + if (!name) continue; + const doc = getDoc(member); + children.push({ + name, + kind: 'variable', + signature: truncate(member.text?.split('\n')[0] ?? ''), + docComment: doc, + body: buildBody(member, doc), + startLine: startLine(member), + endLine: endLine(member), + isExported: isPublic(member), + }); + break; + } + } + } + return children; +} + +function processDeclaration(node: TSNode): ExtractedSymbol[] { + switch (node.type) { + case 'class_declaration': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + const body = node.childForFieldName('body'); + const children = extractTypeMembers(body); + return [{ + name, + kind: 'class', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: isPublic(node), + children: children.length > 0 ? children : undefined, + }]; + } + case 'interface_declaration': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'interface', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: isPublic(node), + }]; + } + case 'struct_declaration': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + const body = node.childForFieldName('body'); + const children = extractTypeMembers(body); + return [{ + name, + kind: 'type', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: isPublic(node), + children: children.length > 0 ? children : undefined, + }]; + } + case 'enum_declaration': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'enum', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: isPublic(node), + }]; + } + case 'method_declaration': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'function', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: isPublic(node), + }]; + } + case 'namespace_declaration': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + const body = node.childForFieldName('body'); + // Recurse into namespace body + const inner: ExtractedSymbol[] = []; + for (const child of body?.namedChildren ?? []) { + inner.push(...processDeclaration(child)); + } + return [{ + name, + kind: 'interface', + signature: truncate(node.text?.split('\n')[0] ?? ''), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + children: inner.length > 0 ? inner : undefined, + }]; + } + default: + return []; + } +} + +const csharpMapper: LanguageMapper = { + extractSymbols(rootNode: TSNode): ExtractedSymbol[] { + const symbols: ExtractedSymbol[] = []; + + function visit(node: TSNode): void { + const results = processDeclaration(node); + if (results.length > 0) { + symbols.push(...results); + return; + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return symbols; + }, + + extractEdges(rootNode: TSNode): ExtractedEdge[] { + const edges: ExtractedEdge[] = []; + + function visit(node: TSNode): void { + if (node.type === 'class_declaration' || node.type === 'struct_declaration') { + const typeName = node.childForFieldName('name')?.text; + if (typeName) { + const baseList = node.childForFieldName('bases'); + if (baseList) { + for (const base of baseList.namedChildren ?? []) { + const baseName = base.type === 'identifier' ? base.text + : base.childForFieldName('name')?.text ?? base.text; + if (baseName) { + edges.push({ fromName: typeName, toName: baseName, kind: 'extends' }); + } + } + } + } + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return edges; + }, + + extractImports(rootNode: TSNode): ExtractedImport[] { + const imports: ExtractedImport[] = []; + + function visit(node: TSNode): void { + if (node.type === 'using_directive') { + // using_directive: using (static)? name ; + for (const child of node.namedChildren ?? []) { + if (child.type === 'identifier' || child.type === 'qualified_name' || + child.type === 'alias_qualified_name') { + imports.push({ specifier: child.text }); + break; + } + } + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return imports; + }, +}; + +let _registered = false; + +export function registerCsharp(): void { + if (_registered) return; + _registered = true; + registerLanguage('csharp', 'tree-sitter-c-sharp.wasm', csharpMapper); +} diff --git a/src/lib/parsers/languages/gdscript.ts b/src/lib/parsers/languages/gdscript.ts new file mode 100644 index 0000000..9f95831 --- /dev/null +++ b/src/lib/parsers/languages/gdscript.ts @@ -0,0 +1,307 @@ +import type { ExtractedSymbol, ExtractedEdge, ExtractedImport, LanguageMapper } from './types'; +import { registerLanguage } from './registry'; +import { + type TSNode, + truncate, + startLine, + endLine, + sliceBeforeBody, + buildBody, + getPrecedingDoc, +} from './helpers'; + +function getDoc(node: TSNode): string { + return getPrecedingDoc(node, ['comment'], '#'); +} + +function buildSig(node: TSNode): string { + const body = node.childForFieldName('body'); + if (!body) return truncate(node.text ?? ''); + const header = sliceBeforeBody(node, body); + return truncate(header ?? (node.text ?? '').split('\n')[0]); +} + +/** Extract the base class name from extends_statement. */ +function getExtendsName(node: TSNode): string | null { + const ext = node.childForFieldName('extends'); + if (!ext) return null; + // extends_statement children: type or string + for (const c of ext.namedChildren ?? []) { + if (c.type === 'type' || c.type === 'string' || c.type === 'identifier') { + return c.text.replace(/^["']|["']$/g, ''); + } + } + return ext.text.replace(/^["']|["']$/g, '') || null; +} + +function extractClassMembers(body: TSNode): ExtractedSymbol[] { + const children: ExtractedSymbol[] = []; + if (!body) return children; + + for (const stmt of body.namedChildren ?? []) { + switch (stmt.type) { + case 'function_definition': { + const name = stmt.childForFieldName('name')?.text ?? ''; + if (!name) continue; + const doc = getDoc(stmt); + children.push({ + name, + kind: 'method', + signature: buildSig(stmt), + docComment: doc, + body: buildBody(stmt, doc), + startLine: startLine(stmt), + endLine: endLine(stmt), + isExported: !name.startsWith('_'), + }); + break; + } + case 'constructor_definition': { + const doc = getDoc(stmt); + children.push({ + name: '_init', + kind: 'constructor', + signature: buildSig(stmt), + docComment: doc, + body: buildBody(stmt, doc), + startLine: startLine(stmt), + endLine: endLine(stmt), + isExported: false, + }); + break; + } + case 'variable_statement': + case 'export_variable_statement': { + const name = stmt.childForFieldName('name')?.text ?? ''; + if (!name) continue; + const doc = getDoc(stmt); + children.push({ + name, + kind: 'variable', + signature: truncate(stmt.text ?? ''), + docComment: doc, + body: buildBody(stmt, doc), + startLine: startLine(stmt), + endLine: endLine(stmt), + isExported: stmt.type === 'export_variable_statement', + }); + break; + } + case 'signal_statement': { + const name = stmt.childForFieldName('name')?.text ?? ''; + if (!name) continue; + const doc = getDoc(stmt); + children.push({ + name, + kind: 'variable', + signature: truncate(stmt.text ?? ''), + docComment: doc, + body: buildBody(stmt, doc), + startLine: startLine(stmt), + endLine: endLine(stmt), + isExported: true, + }); + break; + } + } + } + return children; +} + +function processTopLevel(node: TSNode): ExtractedSymbol[] { + switch (node.type) { + case 'function_definition': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'function', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: !name.startsWith('_'), + }]; + } + case 'constructor_definition': { + const doc = getDoc(node); + return [{ + name: '_init', + kind: 'function', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: false, + }]; + } + case 'class_definition': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + const body = node.childForFieldName('body'); + const children = extractClassMembers(body); + return [{ + name, + kind: 'class', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + children: children.length > 0 ? children : undefined, + }]; + } + case 'class_name_statement': { + // class_name MyClass [extends Base] — top-level class declaration + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'class', + signature: truncate(node.text ?? ''), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + }]; + } + case 'enum_definition': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'enum', + signature: truncate(node.text?.split('\n')[0] ?? ''), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + }]; + } + case 'signal_statement': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'variable', + signature: truncate(node.text ?? ''), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + }]; + } + case 'variable_statement': + case 'export_variable_statement': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'variable', + signature: truncate(node.text ?? ''), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: node.type === 'export_variable_statement', + }]; + } + case 'const_statement': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'variable', + signature: truncate(node.text ?? ''), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + }]; + } + default: + return []; + } +} + +const gdscriptMapper: LanguageMapper = { + extractSymbols(rootNode: TSNode): ExtractedSymbol[] { + const symbols: ExtractedSymbol[] = []; + for (const child of rootNode.children ?? []) { + symbols.push(...processTopLevel(child)); + } + return symbols; + }, + + extractEdges(rootNode: TSNode): ExtractedEdge[] { + const edges: ExtractedEdge[] = []; + + // class_name_statement extends Base → file-level inheritance + for (const child of rootNode.children ?? []) { + if (child.type === 'class_name_statement') { + const className = child.childForFieldName('name')?.text; + const baseName = getExtendsName(child); + if (className && baseName) { + edges.push({ fromName: className, toName: baseName, kind: 'extends' }); + } + } + if (child.type === 'class_definition') { + const className = child.childForFieldName('name')?.text; + const baseName = getExtendsName(child); + if (className && baseName) { + edges.push({ fromName: className, toName: baseName, kind: 'extends' }); + } + } + } + + return edges; + }, + + extractImports(rootNode: TSNode): ExtractedImport[] { + const imports: ExtractedImport[] = []; + + function visit(node: TSNode): void { + // preload("res://foo.gd") or load("res://foo.gd") + if (node.type === 'call') { + const fn = node.namedChildren?.[0]; + if (fn?.type === 'identifier' && (fn.text === 'preload' || fn.text === 'load')) { + const args = node.childForFieldName('arguments'); + if (args) { + for (const arg of args.namedChildren ?? []) { + if (arg.type === 'string') { + const specifier = arg.text.replace(/^["']|["']$/g, ''); + imports.push({ specifier }); + } + } + } + } + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return imports; + }, +}; + +let _registered = false; + +export function registerGdscript(): void { + if (_registered) return; + _registered = true; + registerLanguage('gdscript', 'tree-sitter-gdscript.wasm', gdscriptMapper); +} diff --git a/src/lib/parsers/languages/go.ts b/src/lib/parsers/languages/go.ts new file mode 100644 index 0000000..89724c9 --- /dev/null +++ b/src/lib/parsers/languages/go.ts @@ -0,0 +1,156 @@ +import type { ExtractedSymbol, ExtractedEdge, ExtractedImport, LanguageMapper } from './types'; +import { registerLanguage } from './registry'; +import { + type TSNode, + truncate, + startLine, + endLine, + sliceBeforeBody, + buildBody, + getPrecedingDoc, +} from './helpers'; + +function getDoc(node: TSNode): string { + return getPrecedingDoc(node, ['comment']); +} + +function buildSig(node: TSNode): string { + const body = node.childForFieldName('body'); + if (!body) return truncate(node.text ?? ''); + const header = sliceBeforeBody(node, body); + return truncate(header ?? (node.text ?? '').split('\n')[0]); +} + +function extractMethods(body: TSNode): ExtractedSymbol[] { + // Go structs don't have methods inside body — methods are top-level with receiver. + // We extract struct fields as children instead. + const children: ExtractedSymbol[] = []; + if (!body) return children; + // field_declaration_list → field_declaration + for (const child of body.namedChildren ?? []) { + if (child.type === 'field_declaration') { + // field has named children: names (field_identifier) and type + for (const n of child.namedChildren ?? []) { + if (n.type === 'field_identifier') { + children.push({ + name: n.text ?? '', + kind: 'variable', + signature: truncate(child.text ?? ''), + docComment: '', + body: child.text ?? '', + startLine: startLine(child), + endLine: endLine(child), + isExported: false, + }); + } + } + } + } + return children; +} + +function processTopLevel(node: TSNode): ExtractedSymbol[] { + switch (node.type) { + case 'function_declaration': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'function', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: /^[A-Z]/.test(name), + }]; + } + case 'method_declaration': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'method', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: /^[A-Z]/.test(name), + }]; + } + case 'type_declaration': { + const symbols: ExtractedSymbol[] = []; + for (const spec of node.namedChildren ?? []) { + if (spec.type !== 'type_spec') continue; + const name = spec.childForFieldName('name')?.text ?? ''; + if (!name) continue; + const typeNode = spec.childForFieldName('type'); + const kind = typeNode?.type === 'struct_type' ? 'class' + : typeNode?.type === 'interface_type' ? 'interface' + : 'type'; + const doc = getDoc(node); + const children = kind === 'class' ? extractMethods(typeNode) : undefined; + symbols.push({ + name, + kind, + signature: truncate(node.text ?? ''), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: /^[A-Z]/.test(name), + children: children && children.length > 0 ? children : undefined, + }); + } + return symbols; + } + default: + return []; + } +} + +const goMapper: LanguageMapper = { + extractSymbols(rootNode: TSNode): ExtractedSymbol[] { + const symbols: ExtractedSymbol[] = []; + for (const child of rootNode.children ?? []) { + symbols.push(...processTopLevel(child)); + } + return symbols; + }, + + extractEdges(_rootNode: TSNode): ExtractedEdge[] { + // Go uses embedding, not traditional inheritance — skip edges + return []; + }, + + extractImports(rootNode: TSNode): ExtractedImport[] { + const imports: ExtractedImport[] = []; + + function collectSpecs(node: TSNode): void { + if (node.type === 'import_spec') { + const path = node.childForFieldName('path'); + if (path) { + const specifier = path.text.replace(/^["'`]|["'`]$/g, ''); + imports.push({ specifier }); + } + } + for (const child of node.namedChildren ?? []) collectSpecs(child); + } + + for (const child of rootNode.children ?? []) { + if (child.type === 'import_declaration') collectSpecs(child); + } + return imports; + }, +}; + +let _registered = false; + +export function registerGo(): void { + if (_registered) return; + _registered = true; + registerLanguage('go', 'tree-sitter-go.wasm', goMapper); +} diff --git a/src/lib/parsers/languages/helpers.ts b/src/lib/parsers/languages/helpers.ts new file mode 100644 index 0000000..c5e225b --- /dev/null +++ b/src/lib/parsers/languages/helpers.ts @@ -0,0 +1,90 @@ +/** + * Shared utilities for tree-sitter language mappers. + */ +import { SIGNATURE_MAX_LEN } from '@/lib/defaults'; + +export type TSNode = any; + +export function truncate(text: string, maxLen = SIGNATURE_MAX_LEN): string { + const collapsed = text.replace(/\s+/g, ' ').trim(); + return collapsed.length > maxLen ? collapsed.slice(0, maxLen) + '…' : collapsed; +} + +export function startLine(node: TSNode): number { + return (node.startPosition?.row ?? 0) + 1; +} + +export function endLine(node: TSNode): number { + return (node.endPosition?.row ?? 0) + 1; +} + +/** + * Slice outerNode.text up to where bodyNode begins (line-based to avoid + * tree-sitter byte-offset vs JS char-offset mismatch). + */ +export function sliceBeforeBody(outerNode: TSNode, bodyNode: TSNode): string | null { + const text = outerNode.text ?? ''; + const outerStartRow = outerNode.startPosition.row; + const bodyStartRow = bodyNode.startPosition.row; + + if (bodyStartRow > outerStartRow) { + const lines = text.split('\n'); + const relativeRow = bodyStartRow - outerStartRow; + const beforeBody = lines.slice(0, relativeRow); + const bodyLine = lines[relativeRow] ?? ''; + const col = bodyNode.startPosition.column; + if (col > 0) beforeBody.push(bodyLine.slice(0, col)); + return beforeBody.join('\n'); + } + + const col = bodyNode.startPosition.column - outerNode.startPosition.column; + if (col > 0) return text.slice(0, col); + return null; +} + +/** Build signature: everything before body node, or first line fallback. */ +export function buildSignature(node: TSNode, bodyFieldName = 'body'): string { + const bodyNode = node.childForFieldName(bodyFieldName); + const text = node.text ?? ''; + if (!bodyNode) return truncate(text); + const header = sliceBeforeBody(node, bodyNode); + return truncate(header ?? text.split('\n')[0]); +} + +export function buildBody(node: TSNode, docComment: string): string { + if (docComment) return docComment + '\n' + (node.text ?? ''); + return node.text ?? ''; +} + +/** + * Find the nearest preceding doc comment. + * @param nodeTypes set of comment node types to accept (default: ['comment']) + * @param prefix required text prefix (e.g. '/**', '///', '#') + */ +export function getPrecedingDoc( + node: TSNode, + nodeTypes: string[] = ['comment'], + prefix?: string, +): string { + const types = new Set(nodeTypes); + let prev = node.previousNamedSibling; + + if (prefix) { + while (prev && types.has(prev.type) && !prev.text.startsWith(prefix)) { + prev = prev.previousNamedSibling; + } + } + + if (prev && types.has(prev.type) && (!prefix || prev.text.startsWith(prefix))) { + return prev.text.trim(); + } + return ''; +} + +/** Walk named children of a node, calling visitor for each. */ +export function walkChildren(node: TSNode, visitor: (child: TSNode) => void): void { + if (!node) return; + for (const child of node.namedChildren ?? []) { + visitor(child); + } +} diff --git a/src/lib/parsers/languages/index.ts b/src/lib/parsers/languages/index.ts index 476f269..48d4c68 100644 --- a/src/lib/parsers/languages/index.ts +++ b/src/lib/parsers/languages/index.ts @@ -18,6 +18,16 @@ export type { ExtractedImport, } from './types'; export { registerTypescript } from './typescript'; +export { registerPython } from './python'; +export { registerGo } from './go'; +export { registerRust } from './rust'; +export { registerJava } from './java'; +export { registerPhp } from './php'; +export { registerRuby } from './ruby'; +export { registerCsharp } from './csharp'; +export { registerCpp } from './cpp'; +export { registerBash } from './bash'; +export { registerGdscript } from './gdscript'; export { createRegexMapper, type RegexMapperOptions, @@ -28,6 +38,27 @@ export { registerRegexLanguages } from './regex-patterns'; // Auto-register built-in languages on import import { registerTypescript } from './typescript'; +import { registerPython } from './python'; +import { registerGo } from './go'; +import { registerRust } from './rust'; +import { registerJava } from './java'; +import { registerPhp } from './php'; +import { registerRuby } from './ruby'; +import { registerCsharp } from './csharp'; +import { registerCpp } from './cpp'; +import { registerBash } from './bash'; +import { registerGdscript } from './gdscript'; import { registerRegexLanguages } from './regex-patterns'; + registerTypescript(); +registerPython(); +registerGo(); +registerRust(); +registerJava(); +registerPhp(); +registerRuby(); +registerCsharp(); +registerCpp(); +registerBash(); +registerGdscript(); registerRegexLanguages(); diff --git a/src/lib/parsers/languages/java.ts b/src/lib/parsers/languages/java.ts new file mode 100644 index 0000000..50923b2 --- /dev/null +++ b/src/lib/parsers/languages/java.ts @@ -0,0 +1,234 @@ +import type { ExtractedSymbol, ExtractedEdge, ExtractedImport, LanguageMapper } from './types'; +import { registerLanguage } from './registry'; +import { + type TSNode, + truncate, + startLine, + endLine, + sliceBeforeBody, + buildBody, + getPrecedingDoc, +} from './helpers'; + +function getDoc(node: TSNode): string { + return getPrecedingDoc(node, ['block_comment', 'line_comment'], '/**') || + getPrecedingDoc(node, ['line_comment']); +} + +function buildSig(node: TSNode): string { + const body = node.childForFieldName('body'); + if (!body) return truncate(node.text ?? ''); + const header = sliceBeforeBody(node, body); + return truncate(header ?? (node.text ?? '').split('\n')[0]); +} + +function isPublic(node: TSNode): boolean { + const mods = node.childForFieldName('modifiers'); + if (!mods) return false; + return mods.text?.includes('public') ?? false; +} + +function extractClassMembers(body: TSNode): ExtractedSymbol[] { + const children: ExtractedSymbol[] = []; + if (!body) return children; + for (const member of body.namedChildren ?? []) { + switch (member.type) { + case 'method_declaration': { + const name = member.childForFieldName('name')?.text ?? ''; + if (!name) continue; + const doc = getDoc(member); + children.push({ + name, + kind: 'method', + signature: buildSig(member), + docComment: doc, + body: buildBody(member, doc), + startLine: startLine(member), + endLine: endLine(member), + isExported: isPublic(member), + }); + break; + } + case 'constructor_declaration': { + const name = member.childForFieldName('name')?.text ?? ''; + if (!name) continue; + const doc = getDoc(member); + children.push({ + name, + kind: 'constructor', + signature: buildSig(member), + docComment: doc, + body: buildBody(member, doc), + startLine: startLine(member), + endLine: endLine(member), + isExported: isPublic(member), + }); + break; + } + case 'field_declaration': { + const decl = member.childForFieldName('declarator'); + const name = decl?.childForFieldName('name')?.text ?? ''; + if (!name) continue; + const doc = getDoc(member); + children.push({ + name, + kind: 'variable', + signature: truncate(member.text ?? ''), + docComment: doc, + body: buildBody(member, doc), + startLine: startLine(member), + endLine: endLine(member), + isExported: isPublic(member), + }); + break; + } + } + } + return children; +} + +function processTopLevel(node: TSNode): ExtractedSymbol[] { + switch (node.type) { + case 'class_declaration': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + const body = node.childForFieldName('body'); + const children = extractClassMembers(body); + return [{ + name, + kind: 'class', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: isPublic(node), + children: children.length > 0 ? children : undefined, + }]; + } + case 'interface_declaration': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'interface', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: isPublic(node), + }]; + } + case 'enum_declaration': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'enum', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: isPublic(node), + }]; + } + case 'annotation_type_declaration': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'type', + signature: truncate(node.text?.split('\n')[0] ?? ''), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: isPublic(node), + }]; + } + default: + return []; + } +} + +const javaMapper: LanguageMapper = { + extractSymbols(rootNode: TSNode): ExtractedSymbol[] { + const symbols: ExtractedSymbol[] = []; + + function visit(node: TSNode): void { + const results = processTopLevel(node); + if (results.length > 0) { + symbols.push(...results); + return; // don't recurse into processed nodes (children already extracted) + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return symbols; + }, + + extractEdges(rootNode: TSNode): ExtractedEdge[] { + const edges: ExtractedEdge[] = []; + + function visit(node: TSNode): void { + if (node.type === 'class_declaration') { + const className = node.childForFieldName('name')?.text; + if (className) { + const superclass = node.childForFieldName('superclass'); + if (superclass) { + // superclass node contains 'extends' keyword + type_identifier + for (const c of superclass.namedChildren ?? []) { + if (c.type === 'type_identifier') { + edges.push({ fromName: className, toName: c.text, kind: 'extends' }); + } + } + } + const interfaces = node.childForFieldName('interfaces'); + if (interfaces) { + for (const c of interfaces.namedChildren ?? []) { + if (c.type === 'type_identifier') { + edges.push({ fromName: className, toName: c.text, kind: 'implements' }); + } + } + } + } + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return edges; + }, + + extractImports(rootNode: TSNode): ExtractedImport[] { + const imports: ExtractedImport[] = []; + for (const child of rootNode.children ?? []) { + if (child.type === 'import_declaration') { + // import_declaration: import (static)? name ; + // name can be scoped_identifier or asterisk + for (const n of child.namedChildren ?? []) { + if (n.type === 'scoped_identifier' || n.type === 'identifier') { + imports.push({ specifier: n.text }); + break; + } + } + } + } + return imports; + }, +}; + +let _registered = false; + +export function registerJava(): void { + if (_registered) return; + _registered = true; + registerLanguage('java', 'tree-sitter-java.wasm', javaMapper); +} diff --git a/src/lib/parsers/languages/php.ts b/src/lib/parsers/languages/php.ts new file mode 100644 index 0000000..37091ba --- /dev/null +++ b/src/lib/parsers/languages/php.ts @@ -0,0 +1,196 @@ +import type { ExtractedSymbol, ExtractedEdge, ExtractedImport, LanguageMapper } from './types'; +import { registerLanguage } from './registry'; +import { + type TSNode, + truncate, + startLine, + endLine, + sliceBeforeBody, + buildBody, + getPrecedingDoc, +} from './helpers'; + +function getDoc(node: TSNode): string { + return getPrecedingDoc(node, ['comment'], '/**') || + getPrecedingDoc(node, ['comment']); +} + +function buildSig(node: TSNode): string { + const body = node.childForFieldName('body'); + if (!body) return truncate(node.text ?? ''); + const header = sliceBeforeBody(node, body); + return truncate(header ?? (node.text ?? '').split('\n')[0]); +} + +function extractClassMembers(body: TSNode): ExtractedSymbol[] { + const children: ExtractedSymbol[] = []; + if (!body) return children; + for (const member of body.namedChildren ?? []) { + if (member.type === 'method_declaration') { + const name = member.childForFieldName('name')?.text ?? ''; + if (!name) continue; + const doc = getDoc(member); + children.push({ + name, + kind: name === '__construct' ? 'constructor' : 'method', + signature: buildSig(member), + docComment: doc, + body: buildBody(member, doc), + startLine: startLine(member), + endLine: endLine(member), + isExported: false, + }); + } + } + return children; +} + +function processTopLevel(node: TSNode): ExtractedSymbol[] { + switch (node.type) { + case 'function_definition': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'function', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + }]; + } + case 'class_declaration': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + const body = node.childForFieldName('body'); + const children = extractClassMembers(body); + return [{ + name, + kind: 'class', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + children: children.length > 0 ? children : undefined, + }]; + } + case 'interface_declaration': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'interface', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + }]; + } + case 'trait_declaration': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + const body = node.childForFieldName('body'); + const children = extractClassMembers(body); + return [{ + name, + kind: 'class', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + children: children.length > 0 ? children : undefined, + }]; + } + default: + return []; + } +} + +const phpMapper: LanguageMapper = { + extractSymbols(rootNode: TSNode): ExtractedSymbol[] { + const symbols: ExtractedSymbol[] = []; + + function visit(node: TSNode): void { + const results = processTopLevel(node); + if (results.length > 0) { + symbols.push(...results); + return; + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return symbols; + }, + + extractEdges(rootNode: TSNode): ExtractedEdge[] { + const edges: ExtractedEdge[] = []; + + function visit(node: TSNode): void { + if (node.type === 'class_declaration') { + const className = node.childForFieldName('name')?.text; + if (className) { + const base = node.childForFieldName('base_clause'); + if (base) { + for (const c of base.namedChildren ?? []) { + if (c.type === 'qualified_name' || c.type === 'name') { + edges.push({ fromName: className, toName: c.text, kind: 'extends' }); + } + } + } + const impl = node.childForFieldName('class_implements'); + if (impl) { + for (const c of impl.namedChildren ?? []) { + if (c.type === 'qualified_name' || c.type === 'name') { + edges.push({ fromName: className, toName: c.text, kind: 'implements' }); + } + } + } + } + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return edges; + }, + + extractImports(rootNode: TSNode): ExtractedImport[] { + const imports: ExtractedImport[] = []; + + function visit(node: TSNode): void { + if (node.type === 'namespace_use_declaration') { + for (const clause of node.namedChildren ?? []) { + if (clause.type === 'namespace_use_clause') { + const name = clause.namedChildren?.[0]; + if (name) imports.push({ specifier: name.text }); + } + } + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return imports; + }, +}; + +let _registered = false; + +export function registerPhp(): void { + if (_registered) return; + _registered = true; + registerLanguage('php', 'tree-sitter-php.wasm', phpMapper); +} diff --git a/src/lib/parsers/languages/python.ts b/src/lib/parsers/languages/python.ts new file mode 100644 index 0000000..95a7a1c --- /dev/null +++ b/src/lib/parsers/languages/python.ts @@ -0,0 +1,193 @@ +import type { ExtractedSymbol, ExtractedEdge, ExtractedImport, LanguageMapper } from './types'; +import { registerLanguage } from './registry'; +import { + type TSNode, + truncate, + startLine, + endLine, + sliceBeforeBody, + buildBody, + getPrecedingDoc, +} from './helpers'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function getDoc(node: TSNode): string { + return getPrecedingDoc(node, ['comment'], '#'); +} + +/** Extract Python docstring from the first statement in a body block. */ +function getDocstring(body: TSNode): string { + if (!body) return ''; + const first = body.namedChildren?.[0]; + if (first?.type === 'expression_statement') { + const str = first.namedChildren?.[0]; + if (str?.type === 'string') return str.text.trim(); + } + return ''; +} + +function buildSig(node: TSNode): string { + const body = node.childForFieldName('body'); + if (!body) return truncate(node.text ?? ''); + const header = sliceBeforeBody(node, body); + return truncate(header ?? (node.text ?? '').split('\n')[0]); +} + +// --------------------------------------------------------------------------- +// Symbol extraction +// --------------------------------------------------------------------------- + +function extractFunctionDef(node: TSNode, doc: string): ExtractedSymbol { + const name = node.childForFieldName('name')?.text ?? ''; + const body = node.childForFieldName('body'); + const docComment = doc || getDocstring(body); + return { + name, + kind: 'function', + signature: buildSig(node), + docComment, + body: buildBody(node, docComment), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + }; +} + +function extractClassMethods(classBody: TSNode): ExtractedSymbol[] { + const children: ExtractedSymbol[] = []; + if (!classBody) return children; + for (const stmt of classBody.namedChildren ?? []) { + let defNode = stmt; + if (stmt.type === 'decorated_definition') { + defNode = stmt.childForFieldName('definition') ?? stmt; + } + if (defNode.type === 'function_definition') { + const name = defNode.childForFieldName('name')?.text ?? ''; + if (!name) continue; + const body = defNode.childForFieldName('body'); + const docComment = getDocstring(body); + children.push({ + name, + kind: name === '__init__' ? 'constructor' : 'method', + signature: buildSig(defNode), + docComment, + body: buildBody(defNode, docComment), + startLine: startLine(defNode), + endLine: endLine(defNode), + isExported: false, + }); + } + } + return children; +} + +function extractClassDef(node: TSNode, doc: string): ExtractedSymbol { + const name = node.childForFieldName('name')?.text ?? ''; + const body = node.childForFieldName('body'); + const docComment = doc || getDocstring(body); + const children = extractClassMethods(body); + return { + name, + kind: 'class', + signature: buildSig(node), + docComment, + body: buildBody(node, docComment), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + children: children.length > 0 ? children : undefined, + }; +} + +function processTopLevel(node: TSNode): ExtractedSymbol[] { + switch (node.type) { + case 'function_definition': + return [extractFunctionDef(node, getDoc(node))]; + case 'class_definition': + return [extractClassDef(node, getDoc(node))]; + case 'decorated_definition': { + const inner = node.childForFieldName('definition'); + if (!inner) return []; + const doc = getDoc(node); + if (inner.type === 'function_definition') return [extractFunctionDef(inner, doc)]; + if (inner.type === 'class_definition') return [extractClassDef(inner, doc)]; + return []; + } + default: + return []; + } +} + +// --------------------------------------------------------------------------- +// Main mapper +// --------------------------------------------------------------------------- + +const pythonMapper: LanguageMapper = { + extractSymbols(rootNode: TSNode): ExtractedSymbol[] { + const symbols: ExtractedSymbol[] = []; + for (const child of rootNode.children ?? []) { + symbols.push(...processTopLevel(child)); + } + return symbols; + }, + + extractEdges(rootNode: TSNode): ExtractedEdge[] { + const edges: ExtractedEdge[] = []; + + function visit(node: TSNode): void { + if (node.type === 'class_definition') { + const className = node.childForFieldName('name')?.text; + if (className) { + const superclasses = node.childForFieldName('superclasses'); + if (superclasses) { + for (const arg of superclasses.namedChildren ?? []) { + const baseName = arg.type === 'identifier' ? arg.text + : arg.type === 'attribute' ? arg.childForFieldName('attribute')?.text ?? arg.text + : null; + if (baseName && baseName !== 'object') { + edges.push({ fromName: className, toName: baseName, kind: 'extends' }); + } + } + } + } + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return edges; + }, + + extractImports(rootNode: TSNode): ExtractedImport[] { + const imports: ExtractedImport[] = []; + for (const child of rootNode.children ?? []) { + if (child.type === 'import_statement') { + for (const n of child.namedChildren ?? []) { + const text = n.type === 'dotted_name' ? n.text + : n.type === 'aliased_import' ? n.childForFieldName('name')?.text + : null; + if (text) imports.push({ specifier: text }); + } + } else if (child.type === 'import_from_statement') { + const mod = child.childForFieldName('module_name')?.text; + if (mod) imports.push({ specifier: mod }); + } + } + return imports; + }, +}; + +// --------------------------------------------------------------------------- +// Registration +// --------------------------------------------------------------------------- + +let _registered = false; + +export function registerPython(): void { + if (_registered) return; + _registered = true; + registerLanguage('python', 'tree-sitter-python.wasm', pythonMapper); +} diff --git a/src/lib/parsers/languages/regex-patterns.ts b/src/lib/parsers/languages/regex-patterns.ts index df324fc..652d892 100644 --- a/src/lib/parsers/languages/regex-patterns.ts +++ b/src/lib/parsers/languages/regex-patterns.ts @@ -1,13 +1,14 @@ /** - * Built-in regex fallback patterns for languages without tree-sitter support. - * Patterns are best-effort — they don't model every edge case, but they - * extract enough symbols for code search to be useful. + * Built-in regex fallback patterns for languages without tree-sitter WASM grammars. + * Languages with WASM grammars (Python, Go, Rust, Java, PHP, Ruby, C#, C++, Bash, GDScript) + * are handled by dedicated tree-sitter mappers instead. */ import { createRegexMapper } from './regex-mapper'; import { registerRegexLanguage } from './registry'; +import type { ExtractedSymbol, ExtractedImport, RegexLanguageMapper } from './types'; -const HASH_LINE = /^\s*#/; const SLASH_LINE = /^\s*\/\//; +const HASH_LINE = /^\s*#/; const DASH_LINE = /^\s*--/; let _registered = false; @@ -17,75 +18,6 @@ export function registerRegexLanguages(): void { if (_registered) return; _registered = true; - // ---- Python ---- - registerRegexLanguage('python', createRegexMapper({ - docCommentLine: HASH_LINE, - symbols: [ - { kind: 'function', pattern: /^[ \t]*(?:async\s+)?def\s+(?[A-Za-z_]\w*)\s*\(/m }, - { kind: 'class', pattern: /^[ \t]*class\s+(?[A-Za-z_]\w*)\b/m }, - ], - imports: [ - { pattern: /^\s*from\s+(?[\w.]+)\s+import\b/m }, - { pattern: /^\s*import\s+(?[\w.]+)/m }, - ], - })); - - // ---- Go ---- - registerRegexLanguage('go', createRegexMapper({ - docCommentLine: SLASH_LINE, - symbols: [ - { kind: 'function', pattern: /^func\s+(?:\([^)]*\)\s+)?(?[A-Za-z_]\w*)\s*\(/m }, - { kind: 'class', pattern: /^type\s+(?[A-Za-z_]\w*)\s+struct\b/m }, - { kind: 'interface', pattern: /^type\s+(?[A-Za-z_]\w*)\s+interface\b/m }, - { kind: 'type', pattern: /^type\s+(?[A-Za-z_]\w*)\s+[A-Za-z_]/m }, - ], - imports: [ - { pattern: /^\s*import\s+"(?[^"]+)"/m }, - ], - })); - - // ---- Rust ---- - registerRegexLanguage('rust', createRegexMapper({ - docCommentLine: SLASH_LINE, - symbols: [ - { kind: 'function', pattern: /^[ \t]*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(?:const\s+)?fn\s+(?[A-Za-z_]\w*)/m }, - { kind: 'class', pattern: /^[ \t]*(?:pub(?:\([^)]*\))?\s+)?struct\s+(?[A-Za-z_]\w*)/m }, - { kind: 'enum', pattern: /^[ \t]*(?:pub(?:\([^)]*\))?\s+)?enum\s+(?[A-Za-z_]\w*)/m }, - { kind: 'interface', pattern: /^[ \t]*(?:pub(?:\([^)]*\))?\s+)?trait\s+(?[A-Za-z_]\w*)/m }, - { kind: 'type', pattern: /^[ \t]*(?:pub(?:\([^)]*\))?\s+)?type\s+(?[A-Za-z_]\w*)/m }, - ], - imports: [ - { pattern: /^\s*use\s+(?[\w:]+)/m }, - ], - })); - - // ---- Ruby ---- - registerRegexLanguage('ruby', createRegexMapper({ - docCommentLine: HASH_LINE, - symbols: [ - { kind: 'function', pattern: /^[ \t]*def\s+(?:self\.)?(?[A-Za-z_]\w*[!?=]?)/m }, - { kind: 'class', pattern: /^[ \t]*class\s+(?[A-Z]\w*)/m }, - { kind: 'interface', pattern: /^[ \t]*module\s+(?[A-Z]\w*)/m }, - ], - imports: [ - { pattern: /^\s*require\s+['"](?[^'"]+)['"]/m }, - { pattern: /^\s*require_relative\s+['"](?[^'"]+)['"]/m }, - ], - })); - - // ---- Java ---- - registerRegexLanguage('java', createRegexMapper({ - docCommentLine: SLASH_LINE, - symbols: [ - { kind: 'class', pattern: /^[ \t]*(?:public\s+|protected\s+|private\s+|abstract\s+|final\s+|static\s+)*class\s+(?[A-Za-z_]\w*)/m }, - { kind: 'interface', pattern: /^[ \t]*(?:public\s+|protected\s+|private\s+)*interface\s+(?[A-Za-z_]\w*)/m }, - { kind: 'enum', pattern: /^[ \t]*(?:public\s+|protected\s+|private\s+)*enum\s+(?[A-Za-z_]\w*)/m }, - ], - imports: [ - { pattern: /^\s*import\s+(?:static\s+)?(?[\w.]+)\s*;/m }, - ], - })); - // ---- Kotlin ---- registerRegexLanguage('kotlin', createRegexMapper({ docCommentLine: SLASH_LINE, @@ -100,35 +32,6 @@ export function registerRegexLanguages(): void { ], })); - // ---- C / C++ ---- - const cMapper = createRegexMapper({ - docCommentLine: SLASH_LINE, - symbols: [ - { kind: 'class', pattern: /^[ \t]*(?:typedef\s+)?struct\s+(?[A-Za-z_]\w*)\s*\{/m }, - { kind: 'class', pattern: /^[ \t]*class\s+(?[A-Za-z_]\w*)\b/m }, - { kind: 'enum', pattern: /^[ \t]*(?:typedef\s+)?enum\s+(?:class\s+)?(?[A-Za-z_]\w*)\b/m }, - ], - imports: [ - { pattern: /^\s*#\s*include\s*[<"](?[^>"]+)[>"]/m }, - ], - }); - registerRegexLanguage('c', cMapper); - registerRegexLanguage('cpp', cMapper); - - // ---- C# ---- - registerRegexLanguage('csharp', createRegexMapper({ - docCommentLine: SLASH_LINE, - symbols: [ - { kind: 'class', pattern: /^[ \t]*(?:public\s+|private\s+|internal\s+|protected\s+|abstract\s+|sealed\s+|static\s+|partial\s+)*class\s+(?[A-Za-z_]\w*)/m }, - { kind: 'interface', pattern: /^[ \t]*(?:public\s+|private\s+|internal\s+|protected\s+)*interface\s+(?I[A-Za-z_]\w*)/m }, - { kind: 'enum', pattern: /^[ \t]*(?:public\s+|private\s+|internal\s+)*enum\s+(?[A-Za-z_]\w*)/m }, - { kind: 'type', pattern: /^[ \t]*(?:public\s+|private\s+|internal\s+)*struct\s+(?[A-Za-z_]\w*)/m }, - ], - imports: [ - { pattern: /^\s*using\s+(?[\w.]+)\s*;/m }, - ], - })); - // ---- Swift ---- registerRegexLanguage('swift', createRegexMapper({ docCommentLine: SLASH_LINE, @@ -144,19 +47,6 @@ export function registerRegexLanguages(): void { ], })); - // ---- PHP ---- - registerRegexLanguage('php', createRegexMapper({ - docCommentLine: SLASH_LINE, - symbols: [ - { kind: 'function', pattern: /^[ \t]*(?:public\s+|private\s+|protected\s+|static\s+|abstract\s+|final\s+)*function\s+(?[A-Za-z_]\w*)/m }, - { kind: 'class', pattern: /^[ \t]*(?:abstract\s+|final\s+)*class\s+(?[A-Za-z_]\w*)/m }, - { kind: 'interface', pattern: /^[ \t]*interface\s+(?[A-Za-z_]\w*)/m }, - ], - imports: [ - { pattern: /^\s*use\s+(?[\w\\]+)/m }, - ], - })); - // ---- Lua ---- registerRegexLanguage('lua', createRegexMapper({ docCommentLine: DASH_LINE, @@ -169,22 +59,6 @@ export function registerRegexLanguages(): void { ], })); - // ---- GDScript (Godot) ---- - registerRegexLanguage('gdscript', createRegexMapper({ - docCommentLine: HASH_LINE, - symbols: [ - { kind: 'function', pattern: /^[ \t]*(?:static\s+)?func\s+(?_?[A-Za-z_]\w*)\s*\(/m }, - { kind: 'class', pattern: /^[ \t]*class\s+(?[A-Za-z_]\w*)/m }, - { kind: 'class', pattern: /^[ \t]*class_name\s+(?[A-Za-z_]\w*)/m }, - { kind: 'enum', pattern: /^[ \t]*enum\s+(?[A-Za-z_]\w*)/m }, - { kind: 'variable', pattern: /^[ \t]*signal\s+(?[A-Za-z_]\w*)/m }, - { kind: 'variable', pattern: /^[ \t]*@export(?:\([^)]*\))?\s+(?:var|onready\s+var)\s+(?[A-Za-z_]\w*)/m }, - ], - imports: [ - { pattern: /\b(?:preload|load)\s*\(\s*['"](?[^'"]+)['"]/m }, - ], - })); - // ---- GLSL / shader ---- const glslMapper = createRegexMapper({ docCommentLine: SLASH_LINE, @@ -211,17 +85,6 @@ export function registerRegexLanguages(): void { ], })); - // ---- Shell / Bash ---- - registerRegexLanguage('shell', createRegexMapper({ - docCommentLine: HASH_LINE, - symbols: [ - { kind: 'function', pattern: /^[ \t]*(?:function\s+)?(?[A-Za-z_]\w*)\s*\(\s*\)\s*\{/m }, - ], - imports: [ - { pattern: /^\s*(?:source|\.)\s+(?[^\s;]+)/m }, - ], - })); - // ---- SQL ---- registerRegexLanguage('sql', createRegexMapper({ docCommentLine: DASH_LINE, @@ -271,4 +134,153 @@ export function registerRegexLanguages(): void { { pattern: /^\s*import\s+(?:qualified\s+)?(?[\w.]+)/m }, ], })); + + // ---- Godot Scene (.tscn / .escn) ---- + // Custom mapper: nodes can share names under different parents, so we use + // the full node path (parent/name) as the unique symbol identifier. + registerRegexLanguage('godot-scene', ((): RegexLanguageMapper => { + const NODE_RE = /^\[node name="([^"]+)"(?:[^\]]*? type="([^"]*)")?(?:[^\]]*? parent="([^"]*)")?\]/gm; + const SUB_RE = /^\[sub_resource type="([^"]*)" id="([^"]*)"/gm; + const CONN_RE = /^\[connection signal="([^"]+)" from="([^"]+)" to="([^"]+)" method="([^"]+)"/gm; + const EXT_RE = /^\[ext_resource [^\]]*path="(res:\/\/[^"]+)"/gm; + + function offsetToLine(src: string, idx: number): number { + let n = 1; + for (let i = 0; i < idx && i < src.length; i++) if (src.charCodeAt(i) === 10) n++; + return n; + } + + return { + extractSymbols(source: string) { + const symbols: ExtractedSymbol[] = []; + const seen = new Set(); + + // Nodes — build full path to avoid duplicate names + NODE_RE.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = NODE_RE.exec(source)) !== null) { + const nodeName = m[1]; + const parent = m[3]; // undefined = root, "." = direct child of root + + let fullPath: string; + if (parent === undefined) { + fullPath = nodeName; // root node + } else if (parent === '.') { + fullPath = nodeName; + } else { + fullPath = `${parent}/${nodeName}`; + } + + // Disambiguate truly duplicate paths (shouldn't happen in valid tscn) + let key = fullPath; + let n = 1; + while (seen.has(key)) key = `${fullPath}#${++n}`; + seen.add(key); + + const line = offsetToLine(source, m.index); + symbols.push({ + name: key, + kind: parent === undefined ? 'class' : 'variable', + signature: m[0], + docComment: '', + body: m[0], + startLine: line, + endLine: line, + isExported: true, + }); + } + + // Sub-resources + SUB_RE.lastIndex = 0; + while ((m = SUB_RE.exec(source)) !== null) { + const id = m[2]; + const type = m[1]; + const key = `${type}::${id}`; + const line = offsetToLine(source, m.index); + symbols.push({ + name: key, + kind: 'variable', + signature: m[0], + docComment: '', + body: m[0], + startLine: line, + endLine: line, + isExported: false, + }); + } + + // Connections + CONN_RE.lastIndex = 0; + while ((m = CONN_RE.exec(source)) !== null) { + const signal = m[1]; + const from = m[2]; + const method = m[4]; + const key = `${from}.${signal}→${method}`; + const line = offsetToLine(source, m.index); + symbols.push({ + name: key, + kind: 'variable', + signature: m[0], + docComment: '', + body: m[0], + startLine: line, + endLine: line, + isExported: false, + }); + } + + symbols.sort((a, b) => a.startLine - b.startLine); + return symbols; + }, + + extractEdges(_source: string) { return []; }, + + extractImports(source: string) { + const out: ExtractedImport[] = []; + EXT_RE.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = EXT_RE.exec(source)) !== null) out.push({ specifier: m[1] }); + return out; + }, + }; + })()); + + // ---- Godot Resource (.tres) ---- + registerRegexLanguage('godot-resource', createRegexMapper({ + docCommentLine: /^\s*;/, + symbols: [ + // Resource type as the main "class" + { kind: 'class', pattern: /^\[gd_resource type="(?[^"]+)"/m }, + // Embedded sub-resources + { kind: 'variable', pattern: /^\[sub_resource type="[^"]*" id="(?[^"]+)"/m }, + ], + imports: [ + { pattern: /^\[ext_resource [^\]]*path="(?res:\/\/[^"]+)"/m }, + ], + })); + + // ---- Godot Project (project.godot) ---- + registerRegexLanguage('godot-project', createRegexMapper({ + docCommentLine: /^\s*;/, + symbols: [ + // Config sections like [application], [rendering/environment/defaults] + { kind: 'variable', pattern: /^\[(?[a-z][a-z0-9_/]*)\]/m }, + ], + imports: [ + // Main scene and other res:// references + { pattern: /=\s*"(?res:\/\/[^"]+)"/m }, + ], + })); + + // ---- Godot Extension (.gdextension) ---- + registerRegexLanguage('godot-extension', createRegexMapper({ + docCommentLine: /^\s*;/, + symbols: [ + // INI sections + { kind: 'variable', pattern: /^\[(?[a-z][a-z0-9_.]*)\]/m }, + ], + imports: [ + { pattern: /=\s*"(?res:\/\/[^"]+)"/m }, + ], + })); } diff --git a/src/lib/parsers/languages/ruby.ts b/src/lib/parsers/languages/ruby.ts new file mode 100644 index 0000000..e2f2fd1 --- /dev/null +++ b/src/lib/parsers/languages/ruby.ts @@ -0,0 +1,191 @@ +import type { ExtractedSymbol, ExtractedEdge, ExtractedImport, LanguageMapper } from './types'; +import { registerLanguage } from './registry'; +import { + type TSNode, + truncate, + startLine, + endLine, + sliceBeforeBody, + buildBody, + getPrecedingDoc, +} from './helpers'; + +function getDoc(node: TSNode): string { + return getPrecedingDoc(node, ['comment'], '#'); +} + +function buildSig(node: TSNode): string { + const body = node.childForFieldName('body'); + if (!body) return truncate(node.text ?? ''); + const header = sliceBeforeBody(node, body); + return truncate(header ?? (node.text ?? '').split('\n')[0]); +} + +function extractBodyMethods(body: TSNode): ExtractedSymbol[] { + const children: ExtractedSymbol[] = []; + if (!body) return children; + for (const stmt of body.namedChildren ?? []) { + if (stmt.type === 'method') { + const name = stmt.childForFieldName('name')?.text ?? ''; + if (!name) continue; + const doc = getDoc(stmt); + children.push({ + name, + kind: name === 'initialize' ? 'constructor' : 'method', + signature: buildSig(stmt), + docComment: doc, + body: buildBody(stmt, doc), + startLine: startLine(stmt), + endLine: endLine(stmt), + isExported: false, + }); + } + } + return children; +} + +function processTopLevel(node: TSNode): ExtractedSymbol[] { + switch (node.type) { + case 'method': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'function', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: /^[a-z]/.test(name), + }]; + } + case 'singleton_method': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'function', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + }]; + } + case 'class': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + const body = node.childForFieldName('body'); + const children = extractBodyMethods(body); + return [{ + name, + kind: 'class', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + children: children.length > 0 ? children : undefined, + }]; + } + case 'module': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'interface', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: true, + }]; + } + default: + return []; + } +} + +const rubyMapper: LanguageMapper = { + extractSymbols(rootNode: TSNode): ExtractedSymbol[] { + const symbols: ExtractedSymbol[] = []; + + function visit(node: TSNode): void { + const results = processTopLevel(node); + if (results.length > 0) { + symbols.push(...results); + return; + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return symbols; + }, + + extractEdges(rootNode: TSNode): ExtractedEdge[] { + const edges: ExtractedEdge[] = []; + + function visit(node: TSNode): void { + if (node.type === 'class') { + const className = node.childForFieldName('name')?.text; + const superclass = node.childForFieldName('superclass'); + if (className && superclass) { + // superclass node: < constant + for (const c of superclass.namedChildren ?? []) { + if (c.type === 'constant' || c.type === 'scope_resolution') { + edges.push({ fromName: className, toName: c.text, kind: 'extends' }); + break; + } + } + } + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return edges; + }, + + extractImports(rootNode: TSNode): ExtractedImport[] { + const imports: ExtractedImport[] = []; + + function visit(node: TSNode): void { + // require 'foo' or require_relative 'foo' → call nodes + if (node.type === 'call') { + const method = node.childForFieldName('method'); + if (method?.text === 'require' || method?.text === 'require_relative') { + const args = node.childForFieldName('arguments'); + if (args) { + for (const arg of args.namedChildren ?? []) { + if (arg.type === 'string') { + const specifier = arg.text.replace(/^['"]|['"]$/g, ''); + imports.push({ specifier }); + } + } + } + } + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return imports; + }, +}; + +let _registered = false; + +export function registerRuby(): void { + if (_registered) return; + _registered = true; + registerLanguage('ruby', 'tree-sitter-ruby.wasm', rubyMapper); +} diff --git a/src/lib/parsers/languages/rust.ts b/src/lib/parsers/languages/rust.ts new file mode 100644 index 0000000..5cce9c9 --- /dev/null +++ b/src/lib/parsers/languages/rust.ts @@ -0,0 +1,218 @@ +import type { ExtractedSymbol, ExtractedEdge, ExtractedImport, LanguageMapper } from './types'; +import { registerLanguage } from './registry'; +import { + type TSNode, + truncate, + startLine, + endLine, + sliceBeforeBody, + buildBody, + getPrecedingDoc, +} from './helpers'; + +function getDoc(node: TSNode): string { + // Rust uses `///` doc comments (line_comment) or `/** */` (block_comment) + return getPrecedingDoc(node, ['line_comment', 'block_comment'], '///') || + getPrecedingDoc(node, ['block_comment'], '/**'); +} + +function buildSig(node: TSNode): string { + const body = node.childForFieldName('body'); + if (!body) return truncate(node.text ?? ''); + const header = sliceBeforeBody(node, body); + return truncate(header ?? (node.text ?? '').split('\n')[0]); +} + +function isPublic(node: TSNode): boolean { + for (const child of node.children ?? []) { + if (child.type === 'visibility_modifier') return true; + } + return false; +} + +function extractImplMethods(body: TSNode): ExtractedSymbol[] { + const children: ExtractedSymbol[] = []; + if (!body) return children; + for (const item of body.namedChildren ?? []) { + if (item.type === 'function_item') { + const name = item.childForFieldName('name')?.text ?? ''; + if (!name) continue; + const doc = getDoc(item); + children.push({ + name, + kind: 'method', + signature: buildSig(item), + docComment: doc, + body: buildBody(item, doc), + startLine: startLine(item), + endLine: endLine(item), + isExported: isPublic(item), + }); + } + } + return children; +} + +function processTopLevel(node: TSNode): ExtractedSymbol[] { + switch (node.type) { + case 'function_item': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'function', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: isPublic(node), + }]; + } + case 'struct_item': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'class', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: isPublic(node), + }]; + } + case 'enum_item': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'enum', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: isPublic(node), + }]; + } + case 'trait_item': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'interface', + signature: buildSig(node), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: isPublic(node), + }]; + } + case 'impl_item': { + // impl Trait for Type or impl Type — extract methods as children of the type + const typeNode = node.childForFieldName('type'); + const traitNode = node.childForFieldName('trait'); + const typeName = typeNode?.text ?? ''; + if (!typeName) return []; + const body = node.childForFieldName('body'); + const children = extractImplMethods(body); + const doc = getDoc(node); + const implName = traitNode ? `${typeName}::${traitNode.text}` : typeName; + return [{ + name: implName, + kind: 'class', + signature: truncate(node.text?.split('\n')[0] ?? ''), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: false, + children: children.length > 0 ? children : undefined, + }]; + } + case 'mod_item': { + const name = node.childForFieldName('name')?.text ?? ''; + if (!name) return []; + const doc = getDoc(node); + return [{ + name, + kind: 'interface', + signature: truncate(node.text?.split('\n')[0] ?? ''), + docComment: doc, + body: buildBody(node, doc), + startLine: startLine(node), + endLine: endLine(node), + isExported: isPublic(node), + }]; + } + default: + return []; + } +} + +const rustMapper: LanguageMapper = { + extractSymbols(rootNode: TSNode): ExtractedSymbol[] { + const symbols: ExtractedSymbol[] = []; + for (const child of rootNode.children ?? []) { + symbols.push(...processTopLevel(child)); + } + return symbols; + }, + + extractEdges(rootNode: TSNode): ExtractedEdge[] { + const edges: ExtractedEdge[] = []; + + function visit(node: TSNode): void { + if (node.type === 'impl_item') { + const typeNode = node.childForFieldName('type'); + const traitNode = node.childForFieldName('trait'); + if (typeNode && traitNode) { + edges.push({ + fromName: typeNode.text ?? '', + toName: traitNode.text ?? '', + kind: 'implements', + }); + } + } + for (const child of node.children ?? []) visit(child); + } + + visit(rootNode); + return edges; + }, + + extractImports(rootNode: TSNode): ExtractedImport[] { + const imports: ExtractedImport[] = []; + + function collectUse(node: TSNode): void { + if (node.type === 'use_declaration') { + const arg = node.childForFieldName('argument'); + if (arg) { + // Get the root crate/path from use tree + const text = arg.text ?? ''; + const top = text.split('::')[0].replace(/[^A-Za-z0-9_]/g, ''); + if (top) imports.push({ specifier: top }); + } + } + for (const child of node.children ?? []) collectUse(child); + } + + collectUse(rootNode); + return imports; + }, +}; + +let _registered = false; + +export function registerRust(): void { + if (_registered) return; + _registered = true; + registerLanguage('rust', 'tree-sitter-rust.wasm', rustMapper); +} diff --git a/wasm/tree-sitter-gdscript.wasm b/wasm/tree-sitter-gdscript.wasm new file mode 100755 index 0000000000000000000000000000000000000000..232f8825b587fd62dc6a859c06f726bf09bd534e GIT binary patch literal 292506 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≀c=NVKY&9tFgU?m?+!vohxYqa- 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 z z&TUoIwhU8yoZreieE73%lMLQC_m4lRT>fQ**E_T%b-7_v?0^Y!xC198wl=L=wc?8@ zE%?x-Su@k5DIbb9Zp@11hGs~8)40B==U!jcGZT3y(~Q(Z(e>gN_(xzkonOMAL24G6 z2jikYj!rPGxXfzJI`B5!x9Z4e;0N;mHahV?3p$&A+}SB&IP_tFOg8dgAx>6a=1zxU{2vXN%hPG%G=*BuV<>P=H^(xoZ~3xr z>5ITmxW3kxRMBg{?QgIBQm>Yj+=?EOzLkCr=a;rh-+B3MeSR-i$Kz|78>^1|PmpcL z$?HoUC$}K~r2)NoAn&b@XLOY3|0`64N>Eq!>+Wra!f(IsoyyXRp3E{`O*cN<8wQ7R zPd^uq;4}IHIL1_Ggj7%2^B+ODYZk*cgHaO4Ho*u?()*d*O)SUgDo?LgAa6x8o{|fR zN0F&wit#-Wf0IJKQcmrT6Mm4$I~vD*S0no=0i!_UCR?+Vu{iYYgETiG<<9r$8m3$a z`NS^B>Uk5b`>;+B!ZxmzZ$j~bmH4;m-maHoGl!HB!%(`vYjcRVkT$X(2!TavuL>DA zLk#2{TV;5JJ9Tq>xdz*EF#aB~Fs983F*E);-n!L`n{)Z;v$}lUeKTKiuR9}Vo}U>r zm+^J-hu~K4YyWQ9Zee@H%|qA6+;ZfNpS+or8))*TaWm=mu}iJE8O2Yq{J?UiD(s)a z&wPHrQ{1$!88;`wP~;ET9u7NWcjPW{vk!Jnpf;=_eF$mA=<2f+wLH{?E6|OF*>DwH z41KU04As#e&GtI-44|wwa1h*rd?xAVQD(KB^Fy3U>bgFQg&_1{1v-@qu+`Bs<12U0^g8UhwX9LT?z$o8Jq^w z;Ubs@Mesa69*1Vwe?h)1(oTTN$a_FdI1v3gq|JwgFc;2)rEn6=gvBr!CO}2<6tX=C z_9br@VsRerYQ%m^Xo7wQ?U3!>&=BI-K8#HseRdPuw54cirLSFt?{YT=_f z`pIl_>DT`FU&a32Fbn-{=+1>rP!V06ZFPv_V=t%%m64N_RT(69)u9GdgfH-EU;s3N zGhr1>g{PrC+yDb8Ybf+!e=|IYuUhzB#`X&QOk=wMeu6_tKaaY6Mf!!OmXY(HRo9@{Y)WrQCMkfj`nS!3!eV_)XYwUl`nTZ#9L)J5tynf$ zoDm;E_IUNIEXm=%bIFo7m_>=*{iN*iiQOFTBIOQmIc$oP z;PRN;X6@RxYTK%POX8B(vS|32x=ltE7mqLM*t+$|yy8(4hPC2mLhJneiQY>~JdEaj zd);>J+KtF*+jc~|VI4-a+jqo%*=`32)fHm@+2 zyC`|?JI3Z!$4+pY+j3-n!7$zrHs5I+Zz&5IMZ8UIIbwXV+<$9cwJ?|Wh4OB$rMyin OYFA-Cks_WuDh&*k6% literal 0 HcmV?d00001 From 5f5f75e9925e260a6f9b421ae13f48b77d95416f Mon Sep 17 00:00:00 2001 From: 2dmaster Date: Tue, 12 May 2026 14:59:14 +0300 Subject: [PATCH 3/5] fix(tests): update tests to reflect tree-sitter promotion of python/go/rust/gdscript MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - multi-config: update expected default codeInclude to include Godot + glsl extensions - regex-parser: python/go/rust/gdscript moved to tree-sitter — remove their regex mapper tests and flip isRegexLanguageSupported assertions to false - code-parser-advanced: use .xyz extension for unsupported-language test (.py now parsed) Co-Authored-By: Claude Sonnet 4.6 --- src/tests/code-parser-advanced.test.ts | 2 +- src/tests/multi-config.test.ts | 2 +- src/tests/regex-parser.test.ts | 91 +++----------------------- 3 files changed, 12 insertions(+), 83 deletions(-) diff --git a/src/tests/code-parser-advanced.test.ts b/src/tests/code-parser-advanced.test.ts index e387878..156964e 100644 --- a/src/tests/code-parser-advanced.test.ts +++ b/src/tests/code-parser-advanced.test.ts @@ -243,7 +243,7 @@ describe('unsupported language file', () => { }); it('returns file-only node for unknown extension', async () => { - const tmpFile = path.join(FIXTURES, '_test.py'); + const tmpFile = path.join(FIXTURES, '_test.xyz'); fs.writeFileSync(tmpFile, 'def foo(): pass\n'); try { const pf = await parseCodeFile(tmpFile, FIXTURES, 1000); diff --git a/src/tests/multi-config.test.ts b/src/tests/multi-config.test.ts index 7709894..29f9028 100644 --- a/src/tests/multi-config.test.ts +++ b/src/tests/multi-config.test.ts @@ -23,7 +23,7 @@ projects: expect(p.projectDir).toBe('/tmp/my-app'); expect(p.graphMemory).toBe('/tmp/my-app/.graph-memory'); expect(p.graphConfigs.docs.include).toBe('**/*.md'); - expect(p.graphConfigs.code.include).toBe('**/*.{js,ts,jsx,tsx,mjs,mts,cjs,cts}'); + expect(p.graphConfigs.code.include).toBe('**/*.{js,ts,jsx,tsx,mjs,mts,cjs,cts,gd,gdshader,gdshaderinc,glsl,tscn,escn,tres,godot,gdextension}'); expect(p.exclude).toContain('**/node_modules/**'); expect(p.chunkDepth).toBe(4); expect(p.embedding.maxChars).toBe(24000); diff --git a/src/tests/regex-parser.test.ts b/src/tests/regex-parser.test.ts index 1e71bf7..71f2f30 100644 --- a/src/tests/regex-parser.test.ts +++ b/src/tests/regex-parser.test.ts @@ -82,99 +82,28 @@ describe('createRegexMapper', () => { }); describe('built-in regex languages', () => { - it('python is registered', () => { - expect(isRegexLanguageSupported('python')).toBe(true); - }); - - it('go is registered', () => { - expect(isRegexLanguageSupported('go')).toBe(true); - }); - - it('rust is registered', () => { - expect(isRegexLanguageSupported('rust')).toBe(true); - }); - - it('gdscript is registered', () => { - expect(isRegexLanguageSupported('gdscript')).toBe(true); - }); - it('glsl is registered', () => { expect(isRegexLanguageSupported('glsl')).toBe(true); }); - it('typescript is NOT regex-registered (handled by tree-sitter)', () => { - expect(isRegexLanguageSupported('typescript')).toBe(false); + it('python is NOT regex-registered (handled by tree-sitter)', () => { + expect(isRegexLanguageSupported('python')).toBe(false); }); - describe('python mapper', () => { - const py = getRegexMapper('python')!; - it('extracts def', () => { - expect(py.extractSymbols('def foo():\n pass\n').map(s => s.name)).toEqual(['foo']); - }); - it('extracts async def', () => { - expect(py.extractSymbols('async def foo():\n pass\n').map(s => s.name)).toEqual(['foo']); - }); - it('extracts class', () => { - expect(py.extractSymbols('class Foo(Base):\n pass\n').map(s => s.name)).toEqual(['Foo']); - }); - it('extracts from … import', () => { - expect(py.extractImports('from os.path import join\n').map(i => i.specifier)).toEqual(['os.path']); - }); - it('extracts plain import', () => { - expect(py.extractImports('import sys\n').map(i => i.specifier)).toEqual(['sys']); - }); + it('go is NOT regex-registered (handled by tree-sitter)', () => { + expect(isRegexLanguageSupported('go')).toBe(false); }); - describe('go mapper', () => { - const go = getRegexMapper('go')!; - it('extracts func', () => { - expect(go.extractSymbols('func Hello() {}\n').map(s => s.name)).toEqual(['Hello']); - }); - it('extracts method receiver func', () => { - expect(go.extractSymbols('func (s *Server) Run() {}\n').map(s => s.name)).toEqual(['Run']); - }); - it('extracts struct type', () => { - const out = go.extractSymbols('type Server struct {}\n'); - expect(out.map(s => s.name)).toEqual(['Server']); - expect(out[0].kind).toBe('class'); - }); - it('extracts import', () => { - expect(go.extractImports('import "fmt"\n').map(i => i.specifier)).toEqual(['fmt']); - }); + it('rust is NOT regex-registered (handled by tree-sitter)', () => { + expect(isRegexLanguageSupported('rust')).toBe(false); }); - describe('rust mapper', () => { - const rs = getRegexMapper('rust')!; - it('extracts pub fn', () => { - expect(rs.extractSymbols('pub fn launch() {}\n').map(s => s.name)).toEqual(['launch']); - }); - it('extracts struct', () => { - expect(rs.extractSymbols('pub struct Engine {}\n').map(s => s.name)).toEqual(['Engine']); - }); - it('extracts trait', () => { - const out = rs.extractSymbols('pub trait Runnable {}\n'); - expect(out[0].kind).toBe('interface'); - }); + it('gdscript is NOT regex-registered (handled by tree-sitter)', () => { + expect(isRegexLanguageSupported('gdscript')).toBe(false); }); - describe('gdscript mapper', () => { - const gd = getRegexMapper('gdscript')!; - it('extracts func', () => { - const src = 'func _ready():\n\tpass\nfunc fire(target):\n\tpass\n'; - expect(gd.extractSymbols(src).map(s => s.name)).toEqual(['_ready', 'fire']); - }); - it('extracts class_name', () => { - const out = gd.extractSymbols('class_name Player\nextends Node\n'); - expect(out.map(s => s.name)).toContain('Player'); - }); - it('extracts signal', () => { - const out = gd.extractSymbols('signal hit_target\n'); - expect(out.map(s => s.name)).toEqual(['hit_target']); - }); - it('extracts preload import', () => { - const out = gd.extractImports('var Bullet = preload("res://scenes/bullet.tscn")\n'); - expect(out.map(i => i.specifier)).toEqual(['res://scenes/bullet.tscn']); - }); + it('typescript is NOT regex-registered (handled by tree-sitter)', () => { + expect(isRegexLanguageSupported('typescript')).toBe(false); }); describe('glsl mapper', () => { From 333ad6227a1a65bd6eea2b70d828781797c4149d Mon Sep 17 00:00:00 2001 From: 2dmaster Date: Tue, 12 May 2026 15:21:19 +0300 Subject: [PATCH 4/5] test: add tree-sitter mapper coverage for 9 languages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers Python, Go, Rust, Java, PHP, Ruby, C#, C/C++, and Bash via parseCodeFile integration tests — fixes codecov/patch failure on PR. Co-Authored-By: Claude Sonnet 4.6 --- src/tests/tree-sitter-languages.test.ts | 350 ++++++++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 src/tests/tree-sitter-languages.test.ts diff --git a/src/tests/tree-sitter-languages.test.ts b/src/tests/tree-sitter-languages.test.ts new file mode 100644 index 0000000..8158f81 --- /dev/null +++ b/src/tests/tree-sitter-languages.test.ts @@ -0,0 +1,350 @@ +import path from 'path'; +import fs from 'fs'; +import { parseCodeFile } from '@/lib/parsers/code'; +import type { ParsedFile } from '@/lib/parsers/code'; + +const FIXTURES = path.join(__dirname, 'fixtures', 'code'); +const MTIME = 1000; + +function names(pf: ParsedFile): string[] { + return pf.nodes.filter(n => n.attrs.kind !== 'file').map(n => n.attrs.name); +} + +function node(pf: ParsedFile, name: string) { + return pf.nodes.find(n => n.attrs.name === name); +} + +async function parse(ext: string, src: string): Promise { + const tmpFile = path.join(FIXTURES, `_lang_test${ext}`); + fs.writeFileSync(tmpFile, src); + try { + return await parseCodeFile(tmpFile, FIXTURES, MTIME); + } finally { + fs.unlinkSync(tmpFile); + } +} + +// --------------------------------------------------------------------------- +// Python +// --------------------------------------------------------------------------- + +describe('python tree-sitter mapper', () => { + const src = ` +def greet(name: str) -> str: + """Say hello.""" + return f"hello {name}" + +class Animal: + """Base animal.""" + def __init__(self, name: str): + self.name = name + + def speak(self) -> str: + return "" + +class Dog(Animal): + def speak(self) -> str: + return "woof" +`.trimStart(); + + let pf: ParsedFile; + beforeAll(async () => { pf = await parse('.py', src); }); + + it('extracts top-level function', () => { expect(names(pf)).toContain('greet'); }); + it('function kind=function', () => { expect(node(pf, 'greet')?.attrs.kind).toBe('function'); }); + it('function has docComment', () => { expect(node(pf, 'greet')?.attrs.docComment).toContain('Say hello'); }); + it('extracts class', () => { expect(names(pf)).toContain('Animal'); }); + it('class kind=class', () => { expect(node(pf, 'Animal')?.attrs.kind).toBe('class'); }); + it('extracts subclass', () => { expect(names(pf)).toContain('Dog'); }); + it('extracts __init__ as constructor', () => { expect(names(pf)).toContain('__init__'); }); + it('constructor kind=constructor', () => { expect(node(pf, '__init__')?.attrs.kind).toBe('constructor'); }); + it('extracts method', () => { expect(names(pf)).toContain('speak'); }); + it('extends edge Dog→Animal', () => { + expect(pf.edges.some(e => e.attrs.kind === 'extends' && e.from.includes('Dog') && e.to.includes('Animal'))).toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +// Go +// --------------------------------------------------------------------------- + +describe('go tree-sitter mapper', () => { + const src = `package main + +type Server struct { +\tHost string +\tPort int +} + +type Runner interface { +\tRun() error +} + +func NewServer(host string) *Server { +\treturn &Server{Host: host} +} + +func (s *Server) Start() error { +\treturn nil +} +`; + + let pf: ParsedFile; + beforeAll(async () => { pf = await parse('.go', src); }); + + it('extracts function', () => { expect(names(pf)).toContain('NewServer'); }); + it('function kind=function', () => { expect(node(pf, 'NewServer')?.attrs.kind).toBe('function'); }); + it('exported func isExported=true', () => { expect(node(pf, 'NewServer')?.attrs.isExported).toBe(true); }); + it('extracts method with receiver', () => { expect(names(pf)).toContain('Start'); }); + it('method kind=method', () => { expect(node(pf, 'Start')?.attrs.kind).toBe('method'); }); + it('extracts struct type as class', () => { expect(node(pf, 'Server')?.attrs.kind).toBe('class'); }); + it('extracts interface type', () => { expect(node(pf, 'Runner')?.attrs.kind).toBe('interface'); }); + it('exported struct isExported=true', () => { expect(node(pf, 'Server')?.attrs.isExported).toBe(true); }); +}); + +// --------------------------------------------------------------------------- +// Rust +// --------------------------------------------------------------------------- + +describe('rust tree-sitter mapper', () => { + const src = `/// A network engine. +pub struct Engine { + pub host: String, +} + +pub trait Runnable { + fn run(&self) -> bool; +} + +pub fn launch(host: &str) -> Engine { + Engine { host: host.to_string() } +} + +impl Engine { + pub fn stop(&self) {} +} +`; + + let pf: ParsedFile; + beforeAll(async () => { pf = await parse('.rs', src); }); + + it('extracts struct', () => { expect(names(pf)).toContain('Engine'); }); + it('struct kind=class', () => { expect(node(pf, 'Engine')?.attrs.kind).toBe('class'); }); + it('doc comment on struct', () => { expect(node(pf, 'Engine')?.attrs.docComment).toContain('network engine'); }); + it('extracts trait', () => { expect(names(pf)).toContain('Runnable'); }); + it('trait kind=interface', () => { expect(node(pf, 'Runnable')?.attrs.kind).toBe('interface'); }); + it('extracts function', () => { expect(names(pf)).toContain('launch'); }); + it('function kind=function', () => { expect(node(pf, 'launch')?.attrs.kind).toBe('function'); }); + it('extracts impl method', () => { expect(names(pf)).toContain('stop'); }); + it('impl method kind=method', () => { expect(node(pf, 'stop')?.attrs.kind).toBe('method'); }); +}); + +// --------------------------------------------------------------------------- +// Java +// --------------------------------------------------------------------------- + +describe('java tree-sitter mapper', () => { + const src = `/** + * Base service. + */ +public abstract class BaseService { + protected String name; + + public BaseService(String name) { + this.name = name; + } + + public abstract void start(); +} + +public interface Lifecycle { + void start(); + void stop(); +} +`; + + let pf: ParsedFile; + beforeAll(async () => { pf = await parse('.java', src); }); + + it('extracts class', () => { expect(names(pf)).toContain('BaseService'); }); + it('class kind=class', () => { expect(node(pf, 'BaseService')?.attrs.kind).toBe('class'); }); + it('class docComment', () => { expect(node(pf, 'BaseService')?.attrs.docComment).toContain('Base service'); }); + it('extracts interface', () => { expect(names(pf)).toContain('Lifecycle'); }); + it('interface kind=interface', () => { expect(node(pf, 'Lifecycle')?.attrs.kind).toBe('interface'); }); + it('extracts constructor', () => { expect(names(pf)).toContain('BaseService'); }); + it('extracts method', () => { expect(names(pf)).toContain('start'); }); +}); + +// --------------------------------------------------------------------------- +// PHP +// --------------------------------------------------------------------------- + +describe('php tree-sitter mapper', () => { + const src = `name = $name; + } + + public function getName(): string { + return $this->name; + } +} + +interface Repository { + public function find(int $id): mixed; +} +`; + + let pf: ParsedFile; + beforeAll(async () => { pf = await parse('.php', src); }); + + it('extracts function', () => { expect(names(pf)).toContain('greet'); }); + it('function kind=function', () => { expect(node(pf, 'greet')?.attrs.kind).toBe('function'); }); + it('extracts class', () => { expect(names(pf)).toContain('User'); }); + it('class kind=class', () => { expect(node(pf, 'User')?.attrs.kind).toBe('class'); }); + it('extracts interface', () => { expect(names(pf)).toContain('Repository'); }); + it('interface kind=interface', () => { expect(node(pf, 'Repository')?.attrs.kind).toBe('interface'); }); + it('extracts method', () => { expect(names(pf)).toContain('getName'); }); + it('extracts constructor', () => { expect(names(pf)).toContain('__construct'); }); +}); + +// --------------------------------------------------------------------------- +// Ruby +// --------------------------------------------------------------------------- + +describe('ruby tree-sitter mapper', () => { + const src = `class Dog + def initialize(name) + @name = name + end + + def speak + "woof" + end +end + +module Utilities +end + +def top_level_helper + 42 +end +`; + + let pf: ParsedFile; + beforeAll(async () => { pf = await parse('.rb', src); }); + + it('extracts class', () => { expect(names(pf)).toContain('Dog'); }); + it('class kind=class', () => { expect(node(pf, 'Dog')?.attrs.kind).toBe('class'); }); + it('extracts initialize as constructor', () => { expect(names(pf)).toContain('initialize'); }); + it('constructor kind=constructor', () => { expect(node(pf, 'initialize')?.attrs.kind).toBe('constructor'); }); + it('extracts instance method', () => { expect(names(pf)).toContain('speak'); }); + it('instance method kind=method', () => { expect(node(pf, 'speak')?.attrs.kind).toBe('method'); }); + it('extracts module', () => { expect(names(pf)).toContain('Utilities'); }); + it('module kind=interface', () => { expect(node(pf, 'Utilities')?.attrs.kind).toBe('interface'); }); + it('extracts top-level method', () => { expect(names(pf)).toContain('top_level_helper'); }); + it('top-level method kind=function', () => { expect(node(pf, 'top_level_helper')?.attrs.kind).toBe('function'); }); +}); + +// --------------------------------------------------------------------------- +// C# (csharp) +// --------------------------------------------------------------------------- + +describe('csharp tree-sitter mapper', () => { + const src = `///

Base service class. +public abstract class BaseService { + public BaseService() {} + + /// Start the service. + public abstract void Start(); +} + +public interface ILifecycle { + void Start(); +} + +public struct Point { + public int X; +} +`; + + let pf: ParsedFile; + beforeAll(async () => { pf = await parse('.cs', src); }); + + it('extracts class', () => { expect(names(pf)).toContain('BaseService'); }); + it('class kind=class', () => { expect(node(pf, 'BaseService')?.attrs.kind).toBe('class'); }); + it('extracts interface', () => { expect(names(pf)).toContain('ILifecycle'); }); + it('interface kind=interface', () => { expect(node(pf, 'ILifecycle')?.attrs.kind).toBe('interface'); }); + it('extracts struct as type', () => { expect(node(pf, 'Point')?.attrs.kind).toBe('type'); }); + it('extracts method as child of class', () => { expect(names(pf)).toContain('Start'); }); + it('method kind=method', () => { expect(node(pf, 'Start')?.attrs.kind).toBe('method'); }); + it('extracts constructor', () => { expect(names(pf)).toContain('BaseService'); }); +}); + +// --------------------------------------------------------------------------- +// C / C++ +// --------------------------------------------------------------------------- + +describe('cpp tree-sitter mapper', () => { + const src = `namespace net { + class Server { + public: + void start(); + }; +} + +int add(int a, int b) { + return a + b; +} +`; + + let pf: ParsedFile; + beforeAll(async () => { pf = await parse('.cpp', src); }); + + it('extracts namespace', () => { expect(names(pf)).toContain('net'); }); + it('namespace kind=interface', () => { expect(node(pf, 'net')?.attrs.kind).toBe('interface'); }); + it('extracts class', () => { expect(names(pf)).toContain('Server'); }); + it('class kind=class', () => { expect(node(pf, 'Server')?.attrs.kind).toBe('class'); }); + it('extracts top-level function', () => { expect(names(pf)).toContain('add'); }); + it('function kind=function', () => { expect(node(pf, 'add')?.attrs.kind).toBe('function'); }); +}); + +// --------------------------------------------------------------------------- +// Bash +// --------------------------------------------------------------------------- + +describe('bash tree-sitter mapper', () => { + const src = `#!/usr/bin/env bash + +deploy() { + echo "deploying..." +} + +function rollback { + echo "rolling back" +} + +main() { + deploy + rollback +} + +main "$@" +`; + + let pf: ParsedFile; + beforeAll(async () => { pf = await parse('.sh', src); }); + + it('extracts posix-style function', () => { expect(names(pf)).toContain('deploy'); }); + it('extracts function-keyword function', () => { expect(names(pf)).toContain('rollback'); }); + it('extracts main', () => { expect(names(pf)).toContain('main'); }); + it('function kind=function', () => { expect(node(pf, 'deploy')?.attrs.kind).toBe('function'); }); +}); From ce8c5fce3ec35e9269413d1d158f2d8b52579e12 Mon Sep 17 00:00:00 2001 From: 2dmaster Date: Tue, 12 May 2026 16:14:54 +0300 Subject: [PATCH 5/5] =?UTF-8?q?test:=20expand=20language=20coverage=20?= =?UTF-8?q?=E2=80=94=20GDScript,=20Godot=20files,=20C#/C++=20branches?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GDScript: func, class_name, enum, signal, var, const, inner class, extends edge (7% → 80%) - Godot scene/resource/project/extension regex mappers (regex-patterns.ts 40% → 100%) - C# extended: namespace, enum, property, method, field (68% → 88%) - C++ extended: template class, enum, class methods, extracted via class body (69% → 80%) Co-Authored-By: Claude Sonnet 4.6 --- src/tests/tree-sitter-languages.test.ts | 214 ++++++++++++++++++++++++ 1 file changed, 214 insertions(+) diff --git a/src/tests/tree-sitter-languages.test.ts b/src/tests/tree-sitter-languages.test.ts index 8158f81..21274ca 100644 --- a/src/tests/tree-sitter-languages.test.ts +++ b/src/tests/tree-sitter-languages.test.ts @@ -348,3 +348,217 @@ main "$@" it('extracts main', () => { expect(names(pf)).toContain('main'); }); it('function kind=function', () => { expect(node(pf, 'deploy')?.attrs.kind).toBe('function'); }); }); + +// --------------------------------------------------------------------------- +// GDScript +// --------------------------------------------------------------------------- + +describe('gdscript tree-sitter mapper', () => { + const src = `class_name Player extends CharacterBody2D + +signal health_changed(new_health: int) + +enum State { IDLE, RUNNING, JUMPING } + +const MAX_SPEED: float = 200.0 + +var health: int = 100 + +func _ready() -> void: +\tpass + +func take_damage(amount: int) -> void: +\thealth -= amount + +class Weapon: +\tvar damage: int = 10 +\t +\tfunc fire() -> void: +\t\tpass +`; + + let pf: ParsedFile; + beforeAll(async () => { pf = await parse('.gd', src); }); + + it('extracts class_name as class', () => { expect(names(pf)).toContain('Player'); }); + it('class_name kind=class', () => { expect(node(pf, 'Player')?.attrs.kind).toBe('class'); }); + it('extracts signal', () => { expect(names(pf)).toContain('health_changed'); }); + it('extracts enum', () => { expect(names(pf)).toContain('State'); }); + it('enum kind=enum', () => { expect(node(pf, 'State')?.attrs.kind).toBe('enum'); }); + it('extracts const', () => { expect(names(pf)).toContain('MAX_SPEED'); }); + it('extracts var', () => { expect(names(pf)).toContain('health'); }); + it('extracts function', () => { expect(names(pf)).toContain('take_damage'); }); + it('function kind=function', () => { expect(node(pf, 'take_damage')?.attrs.kind).toBe('function'); }); + it('extracts inner class', () => { expect(names(pf)).toContain('Weapon'); }); + it('inner class kind=class', () => { expect(node(pf, 'Weapon')?.attrs.kind).toBe('class'); }); + it('extends edge Player→CharacterBody2D', () => { + expect(pf.edges.some(e => e.attrs.kind === 'extends' && e.from.includes('Player') && e.to.includes('CharacterBody2D'))).toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +// Godot scene (.tscn) — regex mapper +// --------------------------------------------------------------------------- + +describe('godot-scene regex mapper', () => { + const src = `[gd_scene load_steps=3 format=3] + +[ext_resource type="Script" path="res://scripts/player.gd" id="1_abc"] + +[node name="Player" type="CharacterBody2D"] +script = ExtResource("1_abc") + +[node name="Sprite2D" type="Sprite2D" parent="."] + +[node name="Hitbox" type="CollisionShape2D" parent="HUD/Container"] + +[sub_resource type="CapsuleShape2D" id="shape_1"] + +[connection signal="body_entered" from="Player" to="Player" method="_on_body_entered"] +`; + + let pf: ParsedFile; + beforeAll(async () => { pf = await parse('.tscn', src); }); + + it('extracts root node as class', () => { expect(node(pf, 'Player')?.attrs.kind).toBe('class'); }); + it('extracts child node as variable', () => { expect(node(pf, 'Sprite2D')?.attrs.kind).toBe('variable'); }); + it('extracts deeply nested node with path', () => { expect(names(pf)).toContain('HUD/Container/Hitbox'); }); + it('extracts sub_resource', () => { expect(names(pf).some(n => n.includes('CapsuleShape2D'))).toBe(true); }); + it('extracts connection as variable', () => { expect(names(pf).some(n => n.includes('body_entered'))).toBe(true); }); +}); + +// --------------------------------------------------------------------------- +// Godot resource (.tres) — regex mapper +// --------------------------------------------------------------------------- + +describe('godot-resource regex mapper', () => { + const src = `[gd_resource type="PhysicsMaterial" load_steps=2 format=3] + +[ext_resource type="Texture2D" path="res://textures/ground.png" id="1_xyz"] + +[sub_resource type="CurveTexture" id="curve_1"] + +[resource] +friction = 0.7 +`; + + let pf: ParsedFile; + beforeAll(async () => { pf = await parse('.tres', src); }); + + it('extracts resource type as class', () => { expect(names(pf).some(n => n.includes('PhysicsMaterial'))).toBe(true); }); + it('extracts sub_resource', () => { expect(names(pf).some(n => n.includes('curve_1'))).toBe(true); }); +}); + +// --------------------------------------------------------------------------- +// Godot project (project.godot) — regex mapper +// --------------------------------------------------------------------------- + +describe('godot-project regex mapper', () => { + const src = `config_version=5 + +[application] +config/name="My Game" +run/main_scene="res://scenes/main.tscn" + +[rendering] +renderer/rendering_method="forward_plus" +`; + + let pf: ParsedFile; + beforeAll(async () => { pf = await parse('.godot', src); }); + + it('extracts [application] section', () => { expect(names(pf)).toContain('application'); }); + it('extracts [rendering] section', () => { expect(names(pf)).toContain('rendering'); }); +}); + +// --------------------------------------------------------------------------- +// Godot extension (.gdextension) — regex mapper +// --------------------------------------------------------------------------- + +describe('gdextension regex mapper', () => { + const src = `[configuration] +entry_symbol = "example_library_init" +compatibility_minimum = "4.1" + +[libraries] +linux.x86_64 = "res://bin/example.so" +windows.x86_64 = "res://bin/example.dll" +`; + + let pf: ParsedFile; + beforeAll(async () => { pf = await parse('.gdextension', src); }); + + it('extracts [configuration] section', () => { expect(names(pf)).toContain('configuration'); }); + it('extracts [libraries] section', () => { expect(names(pf)).toContain('libraries'); }); +}); + +// --------------------------------------------------------------------------- +// C# — extended: namespace, enum, field, property, extends edge +// --------------------------------------------------------------------------- + +describe('csharp tree-sitter mapper (extended)', () => { + const src = `using System; + +public class Repository { + private string conn; + public int Timeout { get; set; } + + public void Connect() {} +} + +namespace MyApp { + public class Service {} +} + +public enum Direction { + North, + South +} +`; + + let pf: ParsedFile; + beforeAll(async () => { pf = await parse('.cs', src); }); + + it('extracts top-level class', () => { expect(names(pf)).toContain('Repository'); }); + it('extracts property as variable', () => { expect(names(pf)).toContain('Timeout'); }); + it('extracts method as child of class', () => { expect(names(pf)).toContain('Connect'); }); + it('extracts namespace', () => { expect(names(pf)).toContain('MyApp'); }); + it('namespace kind=interface', () => { expect(node(pf, 'MyApp')?.attrs.kind).toBe('interface'); }); + it('extracts class inside namespace', () => { expect(names(pf)).toContain('Service'); }); + it('extracts enum', () => { expect(names(pf)).toContain('Direction'); }); + it('enum kind=enum', () => { expect(node(pf, 'Direction')?.attrs.kind).toBe('enum'); }); +}); + +// --------------------------------------------------------------------------- +// C++ — extended: class with method body, inheritance, template, enum +// --------------------------------------------------------------------------- + +describe('cpp tree-sitter mapper (extended)', () => { + const src = `class Animal { +public: + virtual void speak() {} +}; + +class Dog : Animal { +public: + void speak() override {} +}; + +template +class Box { +}; + +enum Color { Red, Green, Blue }; +`; + + let pf: ParsedFile; + beforeAll(async () => { pf = await parse('.cpp', src); }); + + it('extracts base class', () => { expect(names(pf)).toContain('Animal'); }); + it('extracts derived class', () => { expect(names(pf)).toContain('Dog'); }); + it('extracts class method as child', () => { expect(names(pf)).toContain('speak'); }); + it('method kind=method', () => { expect(node(pf, 'speak')?.attrs.kind).toBe('method'); }); + it('extracts template class', () => { expect(names(pf)).toContain('Box'); }); + it('extracts enum', () => { expect(names(pf)).toContain('Color'); }); + it('enum kind=enum', () => { expect(node(pf, 'Color')?.attrs.kind).toBe('enum'); }); +});