diff --git a/src/context/index.ts b/src/context/index.ts index 23d6686..b6efd18 100644 --- a/src/context/index.ts +++ b/src/context/index.ts @@ -99,61 +99,15 @@ export const getEntitiesInRange = ( } /** - * Parse import source from an import entity + * Get import source from an import entity * - * Extracts the source module path from import signatures like: - * - `import { foo } from 'module'` - * - `import foo from 'module'` - * - `import * as foo from 'module'` + * Uses the pre-extracted source from AST parsing (works for all languages). * * @param entity - The import entity * @returns The import source or empty string if not found */ -const parseImportSource = (entity: ExtractedEntity): string => { - // Try to extract from signature using regex - // Common patterns: from 'source' or from "source" - const fromMatch = entity.signature.match(/from\s+['"]([^'"]+)['"]/) - if (fromMatch?.[1]) { - return fromMatch[1] - } - - // For CommonJS style: require('source') - const requireMatch = entity.signature.match(/require\s*\(\s*['"]([^'"]+)['"]/) - if (requireMatch?.[1]) { - return requireMatch[1] - } - - return '' -} - -/** - * Check if an import is a default import - * - * @param entity - The import entity - * @returns Whether this is a default import - */ -const isDefaultImport = (entity: ExtractedEntity): boolean => { - // Default import patterns: - // import foo from 'module' - // But NOT: import { foo } from 'module' - // And NOT: import * as foo from 'module' - const signature = entity.signature - return ( - /^import\s+\w+\s+from/.test(signature) && - !/^import\s*\{/.test(signature) && - !/^import\s*\*/.test(signature) - ) -} - -/** - * Check if an import is a namespace import - * - * @param entity - The import entity - * @returns Whether this is a namespace import - */ -const isNamespaceImport = (entity: ExtractedEntity): boolean => { - // Namespace import pattern: import * as foo from 'module' - return /^import\s*\*\s*as\s+\w+/.test(entity.signature) +const getImportSource = (entity: ExtractedEntity): string => { + return entity.source ?? '' } /** @@ -178,9 +132,7 @@ export const getRelevantImports = ( // Map import entity to ImportInfo const mapToImportInfo = (entity: ExtractedEntity): ImportInfo => ({ name: entity.name, - source: parseImportSource(entity), - isDefault: isDefaultImport(entity) || undefined, - isNamespace: isNamespaceImport(entity) || undefined, + source: getImportSource(entity), }) // If not filtering, return all imports diff --git a/src/extract/fallback.ts b/src/extract/fallback.ts index 7aa8263..61a7c9a 100644 --- a/src/extract/fallback.ts +++ b/src/extract/fallback.ts @@ -6,7 +6,7 @@ import type { SyntaxNode, } from '../types' import { extractDocstring } from './docstring' -import { extractName, extractSignature } from './signature' +import { extractImportSource, extractName, extractSignature } from './signature' /** * Node types that represent extractable entities by language @@ -176,6 +176,12 @@ function walkAndExtract( // Extract docstring const docstring = yield* extractDocstring(node, language, code) + // Extract import source for import entities + const source = + entityType === 'import' + ? (extractImportSource(node, language) ?? undefined) + : undefined + // Create entity const entity: ExtractedEntity = { type: entityType, @@ -192,6 +198,7 @@ function walkAndExtract( }, parent: parentName, node, + source, } entities.push(entity) diff --git a/src/extract/index.ts b/src/extract/index.ts index 31eea9a..8d7ec26 100644 --- a/src/extract/index.ts +++ b/src/extract/index.ts @@ -12,7 +12,7 @@ import { getEntityType, } from './fallback' import { type CompiledQuery, loadQuery, loadQuerySync } from './queries' -import { extractName, extractSignature } from './signature' +import { extractImportSource, extractName, extractSignature } from './signature' /** * Error when entity extraction fails @@ -168,6 +168,12 @@ function matchesToEntities( // Find parent entity const parent = findParentEntityName(itemNode, rootNode, language) + // Extract import source for import entities + const source = + entityType === 'import' + ? (extractImportSource(itemNode, language) ?? undefined) + : undefined + const entity: ExtractedEntity = { type: entityType, name, @@ -183,6 +189,7 @@ function matchesToEntities( }, parent, node: itemNode, + source, } entities.push(entity) @@ -359,4 +366,4 @@ export { } from './fallback' export type { CompiledQuery, QueryLoadError } from './queries' export { clearQueryCache, loadQuery, loadQuerySync } from './queries' -export { extractName, extractSignature } from './signature' +export { extractImportSource, extractName, extractSignature } from './signature' diff --git a/src/extract/signature.ts b/src/extract/signature.ts index 60cfb6b..10ee156 100644 --- a/src/extract/signature.ts +++ b/src/extract/signature.ts @@ -356,3 +356,187 @@ export const extractSignature = ( export const getBodyDelimiter = (language: Language): string => { return BODY_DELIMITERS[language] } + +/** + * Node types that represent import source/path by language + */ +const IMPORT_SOURCE_NODE_TYPES: readonly string[] = [ + 'string', + 'string_literal', + 'interpreted_string_literal', // Go + 'source', // Some grammars use this field name +] + +/** + * Extract the import source path from an import AST node + * + * Works for all supported languages by looking at the AST structure: + * - JS/TS: import { foo } from 'source' -> string child + * - Python: from source import foo -> 'module_name' field or dotted_name + * - Rust: use crate::module::item -> scoped_identifier or path + * - Go: import "source" -> interpreted_string_literal + * - Java: import package.Class -> scoped_identifier + * + * @param node - The import AST node + * @param language - The programming language + * @returns The import source path, or null if not found + */ +export const extractImportSource = ( + node: SyntaxNode, + language: Language, +): string | null => { + // Try the 'source' field first (common in many grammars) + const sourceField = node.childForFieldName('source') + if (sourceField) { + return stripQuotes(sourceField.text) + } + + // Language-specific extraction + switch (language) { + case 'typescript': + case 'javascript': { + // Look for string literal child (the 'from "..."' part) + for (const child of node.children) { + if (child.type === 'string') { + return stripQuotes(child.text) + } + } + break + } + + case 'python': { + // For 'from X import Y', look for module_name field or dotted_name + const moduleNameField = node.childForFieldName('module_name') + if (moduleNameField) { + return moduleNameField.text + } + // For 'import X' style + const nameField = node.childForFieldName('name') + if (nameField) { + return nameField.text + } + // Fallback: look for dotted_name + for (const child of node.children) { + if (child.type === 'dotted_name') { + return child.text + } + } + break + } + + case 'rust': { + // For 'use path::to::item', extract the path + // Look for scoped_identifier, use_wildcard, use_list, or identifier + const argumentField = node.childForFieldName('argument') + if (argumentField) { + // Get the path part (everything except the last segment if it's a use_list) + return extractRustUsePath(argumentField) + } + // Fallback: look for children that could be paths + for (const child of node.children) { + if ( + child.type === 'scoped_identifier' || + child.type === 'identifier' || + child.type === 'use_wildcard' + ) { + return extractRustUsePath(child) + } + } + break + } + + case 'go': { + // For 'import "path"', look for import_spec or interpreted_string_literal + for (const child of node.children) { + // Single import: import "fmt" -> has import_spec child + if (child.type === 'import_spec') { + const pathNode = child.childForFieldName('path') + if (pathNode) { + return stripQuotes(pathNode.text) + } + // Fallback: look for string literal in import_spec + for (const specChild of child.children) { + if (specChild.type === 'interpreted_string_literal') { + return stripQuotes(specChild.text) + } + } + } + // Direct string literal (some Go grammars) + if (child.type === 'interpreted_string_literal') { + return stripQuotes(child.text) + } + // For import blocks: import ( "fmt" "os" ) + if (child.type === 'import_spec_list') { + for (const spec of child.children) { + if (spec.type === 'import_spec') { + const pathNode = spec.childForFieldName('path') + if (pathNode) { + return stripQuotes(pathNode.text) + } + } + } + } + } + break + } + + case 'java': { + // For 'import package.Class', look for scoped_identifier + for (const child of node.children) { + if (child.type === 'scoped_identifier') { + return child.text + } + } + break + } + } + + // Fallback: look for any string-like child + for (const child of node.children) { + if (IMPORT_SOURCE_NODE_TYPES.includes(child.type)) { + return stripQuotes(child.text) + } + } + + return null +} + +/** + * Extract the path from a Rust use declaration + * For 'std::collections::HashMap', returns 'std::collections::HashMap' + * For 'std::collections::{HashMap, HashSet}', returns 'std::collections' + */ +const extractRustUsePath = (node: SyntaxNode): string => { + // If it's a use_list (e.g., {HashMap, HashSet}), get the parent path + if (node.type === 'use_list') { + return '' + } + + // For scoped_identifier, check if the last part is a use_list + if (node.type === 'scoped_identifier') { + const lastChild = node.children[node.children.length - 1] + if (lastChild?.type === 'use_list') { + // Return everything except the use_list + const pathChild = node.childForFieldName('path') + if (pathChild) { + return pathChild.text + } + } + } + + return node.text +} + +/** + * Strip surrounding quotes from a string + */ +const stripQuotes = (str: string): string => { + if ( + (str.startsWith('"') && str.endsWith('"')) || + (str.startsWith("'") && str.endsWith("'")) || + (str.startsWith('`') && str.endsWith('`')) + ) { + return str.slice(1, -1) + } + return str +} diff --git a/src/types.ts b/src/types.ts index d5e020a..822f05c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -100,6 +100,8 @@ export interface ExtractedEntity { parent: string | null /** The underlying AST node */ node: SyntaxNode + /** Import source path (only for import entities) */ + source?: string } /** diff --git a/test/extract.test.ts b/test/extract.test.ts index fa5de4d..92da2a0 100644 --- a/test/extract.test.ts +++ b/test/extract.test.ts @@ -6,6 +6,7 @@ import { extractByNodeTypes, extractEntitiesAsync, extractEntitiesSync, + extractImportSource, getEntityType, loadQuery, loadQuerySync, @@ -836,3 +837,127 @@ export default function defaultFn() { return 2 }` expect(entities).toEqual([]) }) }) + +// ============================================================================ +// Import Source Extraction Tests +// ============================================================================ + +describe('extractImportSource', () => { + test('extracts TypeScript named import source', async () => { + const code = `import { foo, bar } from 'my-module'` + const result = await parseCode(code, 'typescript') + const importNode = result.tree.rootNode.namedChildren[0] + + const source = extractImportSource(importNode, 'typescript') + expect(source).toBe('my-module') + }) + + test('extracts TypeScript default import source', async () => { + const code = `import React from 'react'` + const result = await parseCode(code, 'typescript') + const importNode = result.tree.rootNode.namedChildren[0] + + const source = extractImportSource(importNode, 'typescript') + expect(source).toBe('react') + }) + + test('extracts TypeScript namespace import source', async () => { + const code = `import * as path from 'path'` + const result = await parseCode(code, 'typescript') + const importNode = result.tree.rootNode.namedChildren[0] + + const source = extractImportSource(importNode, 'typescript') + expect(source).toBe('path') + }) + + test('extracts JavaScript import source', async () => { + const code = `import { useState } from 'react'` + const result = await parseCode(code, 'javascript') + const importNode = result.tree.rootNode.namedChildren[0] + + const source = extractImportSource(importNode, 'javascript') + expect(source).toBe('react') + }) + + test('extracts Python from import source', async () => { + const code = `from collections import OrderedDict` + const result = await parseCode(code, 'python') + const importNode = result.tree.rootNode.namedChildren[0] + + const source = extractImportSource(importNode, 'python') + expect(source).toBe('collections') + }) + + test('extracts Python simple import source', async () => { + const code = `import os` + const result = await parseCode(code, 'python') + const importNode = result.tree.rootNode.namedChildren[0] + + const source = extractImportSource(importNode, 'python') + expect(source).toBe('os') + }) + + test('extracts Python dotted import source', async () => { + const code = `from os.path import join` + const result = await parseCode(code, 'python') + const importNode = result.tree.rootNode.namedChildren[0] + + const source = extractImportSource(importNode, 'python') + expect(source).toBe('os.path') + }) + + test('extracts Rust use declaration source', async () => { + const code = `use std::collections::HashMap;` + const result = await parseCode(code, 'rust') + const useNode = result.tree.rootNode.namedChildren[0] + + const source = extractImportSource(useNode, 'rust') + expect(source).toContain('std::collections') + }) + + test('extracts Go import source', async () => { + const code = `package main + +import "fmt"` + const result = await parseCode(code, 'go') + const importNode = result.tree.rootNode.namedChildren.find( + (n) => n.type === 'import_declaration', + ) + + if (importNode) { + const source = extractImportSource(importNode, 'go') + expect(source).toBe('fmt') + } + }) + + test('extracts Java import source', async () => { + const code = `import java.util.List;` + const result = await parseCode(code, 'java') + const importNode = result.tree.rootNode.namedChildren[0] + + const source = extractImportSource(importNode, 'java') + expect(source).toBe('java.util.List') + }) + + test('import entities have source field populated', async () => { + const code = `import { Effect } from 'effect' +import type { Option } from 'effect/Option' + +function test() { return 1 }` + const result = await parseCode(code, 'typescript') + const entities = await extractEntitiesAsync( + result.tree.rootNode, + 'typescript', + code, + ) + + const imports = entities.filter((e) => e.type === 'import') + expect(imports.length).toBeGreaterThan(0) + + // Each import should have source populated + for (const imp of imports) { + expect(imp.source).toBeDefined() + expect(imp.source).not.toBe('') + } + }) +}) diff --git a/test/scope.test.ts b/test/scope.test.ts new file mode 100644 index 0000000..8ad9c7f --- /dev/null +++ b/test/scope.test.ts @@ -0,0 +1,470 @@ +import { beforeAll, describe, expect, test } from 'bun:test' +import { Effect } from 'effect' +import { extractEntitiesAsync } from '../src/extract' +import { initializeParser, parseCode } from '../src/parser' +import { + buildScopeTree, + buildScopeTreeFromEntities, + buildScopeTreeSync, + findScopeAtOffset, + flattenScopeTree, + getAncestorChain, + rangeContains, +} from '../src/scope' +import type { ExtractedEntity, ScopeTree } from '../src/types' + +// ============================================================================ +// Setup +// ============================================================================ + +beforeAll(async () => { + await initializeParser() +}) + +// Helper to parse and extract entities +async function getEntities( + code: string, + language: 'typescript' | 'python' | 'rust' | 'go' | 'java' | 'javascript', +): Promise { + const result = await parseCode(code, language) + return extractEntitiesAsync(result.tree.rootNode, language, code) +} + +// ============================================================================ +// Range Containment Tests +// ============================================================================ + +describe('rangeContains', () => { + test('returns true when outer fully contains inner', () => { + const outer = { start: 0, end: 100 } + const inner = { start: 10, end: 50 } + expect(rangeContains(outer, inner)).toBe(true) + }) + + test('returns true when ranges are equal', () => { + const range = { start: 10, end: 50 } + expect(rangeContains(range, range)).toBe(true) + }) + + test('returns false when inner starts before outer', () => { + const outer = { start: 10, end: 100 } + const inner = { start: 5, end: 50 } + expect(rangeContains(outer, inner)).toBe(false) + }) + + test('returns false when inner ends after outer', () => { + const outer = { start: 0, end: 50 } + const inner = { start: 10, end: 60 } + expect(rangeContains(outer, inner)).toBe(false) + }) + + test('returns false when ranges do not overlap', () => { + const outer = { start: 0, end: 50 } + const inner = { start: 60, end: 100 } + expect(rangeContains(outer, inner)).toBe(false) + }) + + test('returns true when inner is at boundary of outer', () => { + const outer = { start: 0, end: 100 } + const innerAtStart = { start: 0, end: 50 } + const innerAtEnd = { start: 50, end: 100 } + expect(rangeContains(outer, innerAtStart)).toBe(true) + expect(rangeContains(outer, innerAtEnd)).toBe(true) + }) +}) + +// ============================================================================ +// Scope Tree Building Tests +// ============================================================================ + +describe('buildScopeTreeFromEntities', () => { + test('builds tree with single top-level function', async () => { + const code = `function greet(name: string): string { + return \`Hello, \${name}!\` +}` + const entities = await getEntities(code, 'typescript') + const tree = buildScopeTreeFromEntities(entities) + + expect(tree.root.length).toBe(1) + expect(tree.root[0]?.entity.name).toBe('greet') + expect(tree.root[0]?.entity.type).toBe('function') + }) + + test('builds tree with class and nested methods', async () => { + const code = `class Calculator { + add(a: number, b: number): number { + return a + b + } + + subtract(a: number, b: number): number { + return a - b + } +}` + const entities = await getEntities(code, 'typescript') + const tree = buildScopeTreeFromEntities(entities) + + // Should have one root: the class + const classNode = tree.root.find((n) => n.entity.name === 'Calculator') + expect(classNode).toBeDefined() + expect(classNode?.entity.type).toBe('class') + + // Class should have method children + expect(classNode?.children.length).toBe(2) + const methodNames = classNode?.children.map((c) => c.entity.name) + expect(methodNames).toContain('add') + expect(methodNames).toContain('subtract') + }) + + test('separates imports from tree structure', async () => { + const code = `import { Effect } from 'effect' +import type { Option } from 'effect/Option' + +function test() { return 1 }` + const entities = await getEntities(code, 'typescript') + const tree = buildScopeTreeFromEntities(entities) + + // Imports should be in imports array, not in root + expect(tree.imports.length).toBeGreaterThan(0) + expect(tree.imports.every((e) => e.type === 'import')).toBe(true) + + // Root should have the function + const fnNode = tree.root.find((n) => n.entity.name === 'test') + expect(fnNode).toBeDefined() + }) + + test('separates exports from tree structure', async () => { + const code = `export function publicFn() { return 1 } +export default function defaultFn() { return 2 }` + const entities = await getEntities(code, 'typescript') + const tree = buildScopeTreeFromEntities(entities) + + // Exports should be captured + expect(tree.exports.length).toBeGreaterThanOrEqual(0) // May vary by query + }) + + test('handles deeply nested structures', async () => { + const code = `class Outer { + innerMethod() { + function nestedFn() { + return 1 + } + return nestedFn() + } +}` + const entities = await getEntities(code, 'typescript') + const tree = buildScopeTreeFromEntities(entities) + + // Should have class at root + const outerClass = tree.root.find((n) => n.entity.name === 'Outer') + expect(outerClass).toBeDefined() + }) + + test('allEntities contains all extracted entities', async () => { + const code = `import { foo } from 'bar' + +class MyClass { + method() { return 1 } +} + +function standalone() { return 2 }` + const entities = await getEntities(code, 'typescript') + const tree = buildScopeTreeFromEntities(entities) + + // allEntities should have everything + expect(tree.allEntities.length).toBe(entities.length) + }) + + test('handles empty entity list', () => { + const tree = buildScopeTreeFromEntities([]) + + expect(tree.root).toEqual([]) + expect(tree.imports).toEqual([]) + expect(tree.exports).toEqual([]) + expect(tree.allEntities).toEqual([]) + }) +}) + +// ============================================================================ +// buildScopeTree (Effect version) Tests +// ============================================================================ + +describe('buildScopeTree', () => { + test('returns Effect with scope tree', async () => { + const code = `function test() { return 1 }` + const entities = await getEntities(code, 'typescript') + + const tree = await Effect.runPromise(buildScopeTree(entities)) + + expect(tree.root.length).toBe(1) + expect(tree.root[0]?.entity.name).toBe('test') + }) + + test('handles errors gracefully', async () => { + // Even with empty input, should not fail + const tree = await Effect.runPromise(buildScopeTree([])) + expect(tree.root).toEqual([]) + }) +}) + +// ============================================================================ +// buildScopeTreeSync Tests +// ============================================================================ + +describe('buildScopeTreeSync', () => { + test('builds tree synchronously', async () => { + const code = `class Foo { bar() { return 1 } }` + const entities = await getEntities(code, 'typescript') + + const tree = buildScopeTreeSync(entities) + + expect(tree.root.length).toBeGreaterThan(0) + }) + + test('handles empty input', () => { + const tree = buildScopeTreeSync([]) + expect(tree.root).toEqual([]) + }) +}) + +// ============================================================================ +// findScopeAtOffset Tests +// ============================================================================ + +describe('findScopeAtOffset', () => { + test('finds scope node containing offset', async () => { + const code = `class Calculator { + add(a: number, b: number): number { + return a + b + } +}` + const entities = await getEntities(code, 'typescript') + const tree = buildScopeTreeFromEntities(entities) + + // Offset somewhere inside the add method body + const addMethod = entities.find( + (e) => e.name === 'add' && e.type === 'method', + ) + if (addMethod) { + const midpoint = Math.floor( + (addMethod.byteRange.start + addMethod.byteRange.end) / 2, + ) + const scope = findScopeAtOffset(tree, midpoint) + + expect(scope).not.toBeNull() + expect(scope?.entity.name).toBe('add') + } + }) + + test('finds deepest scope when nested', async () => { + const code = `class Outer { + method() { + return 1 + } +}` + const entities = await getEntities(code, 'typescript') + const tree = buildScopeTreeFromEntities(entities) + + // Find method's byte range + const method = entities.find((e) => e.name === 'method') + if (method) { + const offset = method.byteRange.start + 5 // Inside method + const scope = findScopeAtOffset(tree, offset) + + // Should find the method, not the class + expect(scope?.entity.name).toBe('method') + } + }) + + test('returns null for offset outside all scopes', async () => { + const code = `function test() { return 1 }` + const entities = await getEntities(code, 'typescript') + const tree = buildScopeTreeFromEntities(entities) + + // Very large offset outside file + const scope = findScopeAtOffset(tree, 10000) + expect(scope).toBeNull() + }) + + test('returns null for empty tree', () => { + const tree: ScopeTree = { + root: [], + imports: [], + exports: [], + allEntities: [], + } + + const scope = findScopeAtOffset(tree, 0) + expect(scope).toBeNull() + }) +}) + +// ============================================================================ +// getAncestorChain Tests +// ============================================================================ + +describe('getAncestorChain', () => { + test('returns empty array for root-level node', async () => { + const code = `function standalone() { return 1 }` + const entities = await getEntities(code, 'typescript') + const tree = buildScopeTreeFromEntities(entities) + + const fnNode = tree.root[0] + if (fnNode) { + const ancestors = getAncestorChain(fnNode) + expect(ancestors).toEqual([]) + } + }) + + test('returns parent chain for nested node', async () => { + const code = `class Outer { + method() { return 1 } +}` + const entities = await getEntities(code, 'typescript') + const tree = buildScopeTreeFromEntities(entities) + + // Find the method node + const classNode = tree.root.find((n) => n.entity.name === 'Outer') + const methodNode = classNode?.children.find( + (n) => n.entity.name === 'method', + ) + + if (methodNode) { + const ancestors = getAncestorChain(methodNode) + expect(ancestors.length).toBe(1) + expect(ancestors[0]?.entity.name).toBe('Outer') + } + }) +}) + +// ============================================================================ +// flattenScopeTree Tests +// ============================================================================ + +describe('flattenScopeTree', () => { + test('flattens tree to array of all scope nodes', async () => { + const code = `class Outer { + method1() { return 1 } + method2() { return 2 } +} + +function standalone() { return 3 }` + const entities = await getEntities(code, 'typescript') + const tree = buildScopeTreeFromEntities(entities) + + const flattened = flattenScopeTree(tree) + + // Should include class, both methods, and standalone function + const names = flattened.map((n) => n.entity.name) + expect(names).toContain('Outer') + expect(names).toContain('method1') + expect(names).toContain('method2') + expect(names).toContain('standalone') + }) + + test('returns empty array for empty tree', () => { + const tree: ScopeTree = { + root: [], + imports: [], + exports: [], + allEntities: [], + } + + const flattened = flattenScopeTree(tree) + expect(flattened).toEqual([]) + }) +}) + +// ============================================================================ +// Parent/Child Relationship Tests +// ============================================================================ + +describe('parent/child relationships', () => { + test('child nodes have parent reference set', async () => { + const code = `class Parent { + child() { return 1 } +}` + const entities = await getEntities(code, 'typescript') + const tree = buildScopeTreeFromEntities(entities) + + const parentNode = tree.root.find((n) => n.entity.name === 'Parent') + const childNode = parentNode?.children[0] + + expect(childNode?.parent).toBe(parentNode) + }) + + test('root nodes have null parent', async () => { + const code = `function root() { return 1 }` + const entities = await getEntities(code, 'typescript') + const tree = buildScopeTreeFromEntities(entities) + + expect(tree.root[0]?.parent).toBeNull() + }) +}) + +// ============================================================================ +// Multi-language Scope Tree Tests +// ============================================================================ + +describe('multi-language scope trees', () => { + test('builds scope tree for Python', async () => { + const code = `class Calculator: + def add(self, a, b): + return a + b + + def subtract(self, a, b): + return a - b` + const entities = await getEntities(code, 'python') + const tree = buildScopeTreeFromEntities(entities) + + const cls = tree.root.find((n) => n.entity.name === 'Calculator') + expect(cls).toBeDefined() + expect(cls?.children.length).toBe(2) + }) + + test('builds scope tree for Rust', async () => { + const code = `struct Calculator {} + +impl Calculator { + fn add(&self, a: i32, b: i32) -> i32 { + a + b + } +}` + const entities = await getEntities(code, 'rust') + const tree = buildScopeTreeFromEntities(entities) + + // Should have struct and/or impl at root + expect(tree.root.length).toBeGreaterThan(0) + }) + + test('builds scope tree for Go', async () => { + const code = `package main + +func add(a, b int) int { + return a + b +} + +func subtract(a, b int) int { + return a - b +}` + const entities = await getEntities(code, 'go') + const tree = buildScopeTreeFromEntities(entities) + + // Should have both functions at root + const fnNames = tree.root.map((n) => n.entity.name) + expect(fnNames).toContain('add') + expect(fnNames).toContain('subtract') + }) + + test('builds scope tree for Java', async () => { + const code = `public class Calculator { + public int add(int a, int b) { + return a + b; + } +}` + const entities = await getEntities(code, 'java') + const tree = buildScopeTreeFromEntities(entities) + + const cls = tree.root.find((n) => n.entity.name === 'Calculator') + expect(cls).toBeDefined() + }) +})