Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions src/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1000,12 +1000,15 @@ export class Lexer {
if (ctx === LexContext.CommandStart) {
if (!hasExpansions && !quoted) {
const fc = text.charCodeAt(0);
if (
((fc >= CH_a && fc <= CH_z && text.length <= 8) || fc === CH_BANG || fc === CH_LBRACE || fc === CH_RBRACE) &&
text in RESERVED_WORDS
) {
setToken(out, RESERVED_WORDS[text], text, tokenStart, wordEnd);
return;
if ((fc >= CH_a && fc <= CH_z && text.length <= 8) || fc === CH_BANG || fc === CH_LBRACE || fc === CH_RBRACE) {
// Single lookup, then a typeof check — `text in RESERVED_WORDS` would also match
// inherited Object.prototype members, making `toString` and `valueOf` parse as
// keywords. Own reserved words are always numeric Token values.
const reserved = RESERVED_WORDS[text];
if (typeof reserved === "number") {
setToken(out, reserved, text, tokenStart, wordEnd);
return;
}
}
if (fc === CH_LBRACKET && text === "[[") {
setToken(out, Token.DblLBracket, text, tokenStart, wordEnd);
Expand Down
17 changes: 17 additions & 0 deletions test/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,23 @@ test("set and trap commands", () => {
assert.equal(ast.commands.length, 4);
});

// Command names that collide with Object.prototype members must not be mistaken
// for reserved words by the keyword lookup.
test("Object.prototype member names are ordinary command names", () => {
for (const name of ["toString", "valueOf", "constructor", "hasOwnProperty", "__proto__", "isPrototypeOf"]) {
const ast = parse(`${name} arg`);
assert.equal(ast.commands.length, 1, `no command for: ${name}`);
assert.equal(getCmd(ast).name?.text, name);
assert.deepEqual(args(getCmd(ast)), ["arg"]);
}
});

test("Object.prototype member name does not truncate the rest of the script", () => {
const ast = parse("echo hi; toString foo; echo bye");
assert.equal(ast.commands.length, 3);
assert.equal(getCmd(ast, 2).name?.text, "echo");
});

// ── Real-world patterns ─────────────────────────────────────────────

test("real-world scripts parse without errors", () => {
Expand Down
Loading