Files

147 lines
4.9 KiB
TypeScript

import { homedir } from "node:os";
import { describe, expect, it } from "vitest";
import { getParser, type TSNode } from "#src/access-intent/bash/parser";
import { resolvePlainVariableExpansion } from "#src/access-intent/bash/shell-variable-expansion";
import { makeTSNode } from "#test/helpers/fake-ts-node";
/** `$NAME` as tree-sitter-bash builds it: a `$` delimiter plus the name. */
function simpleExpansion(name: string): TSNode {
return makeTSNode("simple_expansion", `$${name}`, [
makeTSNode("$", "$"),
makeTSNode("variable_name", name),
]);
}
/** `${NAME}` as tree-sitter-bash builds it: brace delimiters plus the name. */
function bracedExpansion(name: string): TSNode {
return makeTSNode("expansion", `\${${name}}`, [
makeTSNode("${", "${"),
makeTSNode("variable_name", name),
makeTSNode("}", "}"),
]);
}
function findNodeOfType(node: TSNode, type: string): TSNode | null {
if (node.type === type) return node;
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
const found = child ? findNodeOfType(child, type) : null;
if (found) return found;
}
return null;
}
describe("resolvePlainVariableExpansion", () => {
describe("resolvable variables", () => {
it("resolves $HOME to the OS home directory", () => {
expect(resolvePlainVariableExpansion(simpleExpansion("HOME"))).toBe(
homedir(),
);
});
it("resolves ${HOME} to the OS home directory", () => {
expect(resolvePlainVariableExpansion(bracedExpansion("HOME"))).toBe(
homedir(),
);
});
it("resolves $PWD to the base-relative marker", () => {
// The shell's working directory is the projection's effective base, so
// the base-relative form resolves correctly after any `cd` folding
// without threading a base into this pure function.
expect(resolvePlainVariableExpansion(simpleExpansion("PWD"))).toBe(".");
});
it("resolves ${PWD} to the base-relative marker", () => {
expect(resolvePlainVariableExpansion(bracedExpansion("PWD"))).toBe(".");
});
});
describe("variables outside the resolvable set", () => {
it.each([
"HOMEDIR",
"CURRENT",
"PATH",
"PWDX",
"TMPDIR",
])("leaves $%s unresolved", (name) => {
expect(resolvePlainVariableExpansion(simpleExpansion(name))).toBeNull();
expect(resolvePlainVariableExpansion(bracedExpansion(name))).toBeNull();
});
});
describe("expansions carrying an operator", () => {
it("leaves ${HOME:-/tmp} unresolved", () => {
const node = makeTSNode("expansion", "${HOME:-/tmp}", [
makeTSNode("${", "${"),
makeTSNode("variable_name", "HOME"),
makeTSNode(":-", ":-"),
makeTSNode("word", "/tmp"),
makeTSNode("}", "}"),
]);
expect(resolvePlainVariableExpansion(node)).toBeNull();
});
it("leaves ${#HOME} unresolved", () => {
const node = makeTSNode("expansion", "${#HOME}", [
makeTSNode("${", "${"),
makeTSNode("#", "#"),
makeTSNode("variable_name", "HOME"),
makeTSNode("}", "}"),
]);
expect(resolvePlainVariableExpansion(node)).toBeNull();
});
});
describe("nodes that are not a plain variable reference", () => {
it("returns null for a node with no children", () => {
expect(
resolvePlainVariableExpansion(makeTSNode("simple_expansion", "$HOME")),
).toBeNull();
});
it("returns null for a node with no variable_name child", () => {
const node = makeTSNode("expansion", "${}", [
makeTSNode("${", "${"),
makeTSNode("}", "}"),
]);
expect(resolvePlainVariableExpansion(node)).toBeNull();
});
it("returns null for a variable_assignment naming a resolvable variable", () => {
// `HOME=/tmp` binds the name; it is not a reference to its value.
const node = makeTSNode("variable_assignment", "HOME=/tmp", [
makeTSNode("variable_name", "HOME"),
makeTSNode("=", "="),
makeTSNode("word", "/tmp"),
]);
expect(resolvePlainVariableExpansion(node)).toBeNull();
});
});
describe("fidelity to the shapes tree-sitter-bash actually produces", () => {
it.each([
["ls $HOME", "simple_expansion", homedir()],
["ls ${HOME}", "expansion", homedir()],
["ls $PWD", "simple_expansion", "."],
["ls ${PWD}", "expansion", "."],
["ls ${HOME:-/tmp}", "expansion", null],
["ls ${#HOME}", "expansion", null],
["ls $HOMEDIR", "simple_expansion", null],
])("resolves %s to %s", async (command, nodeType, expected) => {
const parser = await getParser();
const tree = parser.parse(command);
expect(tree).not.toBeNull();
if (!tree) return;
try {
const node = findNodeOfType(tree.rootNode, nodeType);
expect(node).not.toBeNull();
if (!node) return;
expect(resolvePlainVariableExpansion(node)).toBe(expected);
} finally {
tree.delete();
}
});
});
});