152 lines
4.4 KiB
TypeScript
152 lines
4.4 KiB
TypeScript
import { test, expect, describe } from 'bun:test'
|
|
import { EditorScopeAnalyzer } from './editorScopeAnalyzer'
|
|
import { TextDocument } from 'vscode-languageserver-textdocument'
|
|
import { parser } from '../../../src/parser/shrimp'
|
|
import * as Terms from '../../../src/parser/shrimp.terms'
|
|
|
|
describe('EditorScopeAnalyzer', () => {
|
|
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)
|
|
})
|
|
|
|
test('the prelude functions are always in scope', () => {
|
|
const code = `echo "Hello, World!"`
|
|
const { tree, tracker } = parseAndGetScope(code)
|
|
expect(tracker.isInScope('echo', tree.topNode)).toBe(true)
|
|
})
|
|
})
|
|
|
|
const parseAndGetScope = (code: string) => {
|
|
const document = TextDocument.create('test://test.sh', 'shrimp', 1, code)
|
|
const tree = parser.parse(code)
|
|
const tracker = new EditorScopeAnalyzer(document)
|
|
return { document, tree, tracker }
|
|
}
|