import { describe, expect, it } from "vitest"; import type { Pos, SimpleCommand, WhileClause } from "../ast"; import { parse } from "../parse"; function expectDefined(value: T | undefined): T { expect(value).toBeDefined(); if (value === undefined) throw new Error("expected value to be defined"); return value; } describe("position tracking", () => { it("attaches pos/end to the program", () => { const { ast } = parse("foo bar"); expect(ast.pos).toEqual({ offset: 0, line: 1, col: 1 }); expect(ast.end).toEqual({ offset: 7, line: 1, col: 8 }); }); it("attaches pos/end to simple commands and words", () => { const { ast } = parse("foo bar"); const stmt = expectDefined(ast.body[0]); expect(stmt.pos).toEqual({ offset: 0, line: 1, col: 1 }); expect(stmt.end).toEqual({ offset: 7, line: 1, col: 8 }); const cmd = stmt.command as SimpleCommand; expect(cmd.pos).toEqual({ offset: 0, line: 1, col: 1 }); expect(cmd.end).toEqual({ offset: 7, line: 1, col: 8 }); const [foo, bar] = expectDefined(cmd.words); expect(expectDefined(foo).pos).toEqual({ offset: 0, line: 1, col: 1 }); expect(expectDefined(foo).end).toEqual({ offset: 3, line: 1, col: 4 }); expect(expectDefined(bar).pos).toEqual({ offset: 4, line: 1, col: 5 }); expect(expectDefined(bar).end).toEqual({ offset: 7, line: 1, col: 8 }); }); it("tracks line and column across newlines", () => { const { ast } = parse("foo\nbar"); const second = expectDefined(ast.body[1]); expect(second.pos).toEqual({ offset: 4, line: 2, col: 1 }); expect(second.end).toEqual({ offset: 7, line: 2, col: 4 }); }); it("spans control flow statements from keyword to terminator", () => { const src = "while true; do echo hi; done"; const { ast } = parse(src); const wh = expectDefined(ast.body[0]).command as WhileClause; expect(wh.pos).toEqual({ offset: 0, line: 1, col: 1 }); expect(wh.end).toEqual({ offset: src.length, line: 1, col: 29 }); }); it("handles tabs as a single column", () => { const { ast } = parse("\tfoo"); const stmt = expectDefined(ast.body[0]); expect(stmt.pos).toEqual({ offset: 1, line: 1, col: 2 }); }); it("attaches pos/end to expansions inside words", () => { const { ast } = parse("echo $foo"); const cmd = expectDefined(ast.body[0]).command as SimpleCommand; const word = expectDefined(expectDefined(cmd.words)[1]); const expansion = expectDefined(word.parts[0]); expect(expansion.type).toBe("ParamExp"); expect(expansion.pos).toEqual({ offset: 5, line: 1, col: 6 }); expect(expansion.end).toEqual({ offset: 9, line: 1, col: 10 }); }); it("keeps literal positions before escaped characters and continuations", () => { const { ast } = parse("echo a\\ \\" + "\nb"); const cmd = expectDefined(ast.body[0]).command as SimpleCommand; const word = expectDefined(expectDefined(cmd.words)[1]); const part = expectDefined(word.parts[0]); expect(part.type).toBe("Literal"); if (part.type !== "Literal") throw new Error("expected literal part"); expect(part.value).toBe("a b"); expect(part.pos).toEqual({ offset: 5, line: 1, col: 6 }); expect(part.end).toEqual({ offset: 11, line: 2, col: 2 }); }); });