import { test, expect, describe } from 'bun:test' import { ScopeTracker } from './scopeTracker' import { TextDocument } from 'vscode-languageserver-textdocument' import { parser } from '../../../src/parser/shrimp' import * as Terms from '../../../src/parser/shrimp.terms' describe('ScopeTracker', () => { test('top-level assignment is in scope', () => { const code = 'x = 5\necho x' const { tree, tracker } = parseAndGetScope(code) // Find the 'x' identifier in 'echo x' const identifiers: any[] = [] tree.topNode.cursor().iterate((node: any) => { if (node.type.id === Terms.Identifier) { identifiers.push(node.node) } }) // Second identifier should be the 'x' in 'echo x' const xInEcho = identifiers[1] expect(xInEcho).toBeDefined() expect(tracker.isInScope('x', xInEcho)).toBe(true) }) test('undeclared variable is not in scope', () => { const code = 'echo x' const { tree, tracker } = parseAndGetScope(code) // Find the 'x' identifier let xNode: any = null tree.topNode.cursor().iterate((node: any) => { if (node.type.id === Terms.Identifier) { xNode = node.node } }) expect(xNode).toBeDefined() expect(tracker.isInScope('x', xNode)).toBe(false) }) test('function parameter is in scope inside function', () => { const code = `greet = do name: echo name end` const { tree, tracker } = parseAndGetScope(code) // Find all identifiers const identifiers: any[] = [] tree.topNode.cursor().iterate((node: any) => { if (node.type.id === Terms.Identifier) { identifiers.push(node.node) } }) // Find the 'name' in 'echo name' (should be last identifier) const nameInEcho = identifiers[identifiers.length - 1] expect(tracker.isInScope('name', nameInEcho)).toBe(true) }) test('assignment before usage is in scope', () => { const code = `x = 5 y = 10 echo x y` const { tree, tracker } = parseAndGetScope(code) // Find identifiers const identifiers: any[] = [] tree.topNode.cursor().iterate((node: any) => { if (node.type.id === Terms.Identifier) { identifiers.push(node.node) } }) // Last two identifiers should be 'x' and 'y' in 'echo x y' const xInEcho = identifiers[identifiers.length - 2] const yInEcho = identifiers[identifiers.length - 1] expect(tracker.isInScope('x', xInEcho)).toBe(true) expect(tracker.isInScope('y', yInEcho)).toBe(true) }) test('assignment after usage is not in scope', () => { const code = `echo x x = 5` const { tree, tracker } = parseAndGetScope(code) // Find the first 'x' identifier (in echo) let xNode: any = null tree.topNode.cursor().iterate((node: any) => { if (node.type.id === Terms.Identifier && !xNode) { xNode = node.node } }) expect(tracker.isInScope('x', xNode)).toBe(false) }) test('nested function has access to outer scope', () => { const code = `x = 5 greet = do: echo x end` const { tree, tracker } = parseAndGetScope(code) // Find all identifiers const identifiers: any[] = [] tree.topNode.cursor().iterate((node: any) => { if (node.type.id === Terms.Identifier) { identifiers.push(node.node) } }) // Find the 'x' in 'echo x' (should be last identifier) const xInEcho = identifiers[identifiers.length - 1] expect(tracker.isInScope('x', xInEcho)).toBe(true) }) test('inner function parameter shadows outer variable', () => { const code = `x = 5 greet = do x: echo x end` const { tree, tracker } = parseAndGetScope(code) // Find all identifiers const identifiers: any[] = [] tree.topNode.cursor().iterate((node: any) => { if (node.type.id === Terms.Identifier) { identifiers.push(node.node) } }) // The 'x' in 'echo x' should have 'x' in scope (from parameter) const xInEcho = identifiers[identifiers.length - 1] expect(tracker.isInScope('x', xInEcho)).toBe(true) }) }) const parseAndGetScope = (code: string) => { const document = TextDocument.create('test://test.sh', 'shrimp', 1, code) const tree = parser.parse(code) const tracker = new ScopeTracker(document) return { document, tree, tracker } }