shrimp/vscode-extension/server/src/scopeTracker.test.ts
2025-11-05 14:48:12 -08:00

146 lines
4.2 KiB
TypeScript

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 }
}