clarity-pattern-parser
Version:
Parsing Library for Typescript and Javascript.
715 lines (579 loc) • 25.6 kB
text/typescript
import { Options } from "./Options";
import { Literal } from './Literal';
import { Expression } from './Expression';
import { Reference } from "./Reference";
import { Sequence } from './Sequence';
import { Regex } from './Regex';
import { Optional } from "./Optional";
import { AutoComplete } from "../intellisense/AutoComplete";
import { RightAssociated } from "./RightAssociated";
function createExpressionPattern() {
const spaces = new Regex("spaces", "\\s+");
spaces.setTokens([" "]);
const optionalSpaces = new Optional("optional-spaces", spaces);
const variables = new Options("variables", [
new Literal("a", "a"),
new Literal("b", "b"),
new Literal("c", "c"),
new Literal("d", "d"),
new Literal("e", "e"),
]);
const mulDivOperator = new Options("mult-div", [
new Literal("mult", " * "),
new Literal("div", " / "),
]);
const addSubOperator = new Options("add-sub", [
new Literal("add", " + "),
new Literal("sub", " - "),
]);
const boolOperator = new Options("and-or", [
new Literal("and", " && "),
new Literal("or", " || "),
]);
const group = new Sequence("group", [
new Literal("open-paren", "("),
optionalSpaces,
new Reference("expression"),
optionalSpaces,
new Literal("close-paren", ")"),
]);
const ternary = new Sequence("ternary", [
new Reference("expression"),
optionalSpaces,
new Literal("question-mark", "?"),
optionalSpaces,
new Reference("expression"),
optionalSpaces,
new Literal("colon", ":"),
optionalSpaces,
new Reference("expression"),
]);
const multDivExpression = new Sequence("mult-div-expression", [
new Reference("expression"),
mulDivOperator,
new Reference("expression"),
]);
const addSubExpression = new Sequence("add-sub-expression", [
new Reference("expression"),
addSubOperator,
new Reference("expression"),
]);
const boolExpression = new Sequence("bool-expression", [
new Reference("expression"),
boolOperator,
new Reference("expression"),
]);
const expression = new Expression("expression", [
multDivExpression,
addSubExpression,
boolExpression,
new RightAssociated(ternary),
group,
variables,
]);
return expression;
}
function createOptionsExpression() {
const a = new Literal("a", "a");
const b = new Literal("b", "b");
const c = new Literal("c", "c");
const expression = new Expression("expression", [a, b, c]);
return expression;
}
function createTailExpression() {
const a = new Literal("a", "a");
const b = new Literal("b", "b");
const c = new Literal("c", "c");
const variable = new Options("variable", [a, b, c]);
const period = new Literal(".", ".");
const refinement = new Sequence("refinement", [period, variable]);
const refinementExpression = new Sequence("refinement-expression", [
new Reference("expression"),
refinement
]);
const invocation = new Literal("invocation", "()");
const invocationExpression = new Sequence("invocation-expression", [
new Reference("expression"),
invocation
]);
const expression = new Expression("expression", [
refinementExpression,
invocationExpression,
variable
]);
return expression;
}
describe("Expression Pattern", () => {
test("Single Expression", () => {
const expression = createExpressionPattern();
let result = expression.exec("a || c || b / c * a + d");
result = expression.exec("a + b");
result = expression.exec("a + b * c * d");
result = expression.exec("a + b * c || d + e");
result = expression.exec("(a + b) * (c + d)");
result = expression.exec("(a + b) * c + (d + e)");
result = expression.exec("a + b * c ? d : e");
result = expression.exec("a + b * (a + b * c ? d : e) ? d : e");
result = expression.exec("a + b * a + b * c ? d : e ? d : e");
result = expression.exec("a + b * ?");
expect(result).toBe(result);
});
test("Tail", () => {
const expression = createTailExpression();
let result = expression.exec("a");
result = expression.exec("a.b");
result = expression.exec("a.b.c");
result = expression.exec("a.b.c()()()");
expect(result).toBe(result);
});
test("Options like", () => {
const expression = createOptionsExpression();
const autoComplete = new AutoComplete(expression);
const suggestion = autoComplete.suggestFor("a");
expect(suggestion).toBe(suggestion);
});
test("Suggest", () => {
const expression = createExpressionPattern();
const autoComplete = new AutoComplete(expression);
const suggestion = autoComplete.suggestFor("a ? b ");
expect(suggestion).toBe(suggestion);
});
test("Clone With New Name", ()=>{
const expression = createExpressionPattern();
const clone = expression.clone("new-expression");
const result = clone.exec("a || b && c * d");
expect(result).toBe(result);
});
test("Empty patterns throws", () => {
expect(() => new Expression("e", [])).toThrow(
"Need at least one pattern with an 'expression' pattern."
);
});
test("Atom-only expression parses single value and stops", () => {
const expression = createOptionsExpression();
const result = expression.exec("a");
expect(result.ast?.name).toBe("a");
expect(result.cursor.hasError).toBe(false);
});
test("Prefix at end of input", () => {
// Expression: negate is prefix (-expr), atom is a variable
const negate = new Sequence("negate", [
new Literal("minus", "-"),
new Reference("expression"),
]);
const variable = new Literal("variable", "a");
const expression = new Expression("expression", [negate, variable]);
// "-a" means: prefix "-" matches, cursor moves to "a", atom matches,
// cursor has no next -> shouldStopParsing = true
const result = expression.exec("-a");
expect(result.cursor.hasError).toBe(false);
});
test("Infix at end of input", () => {
// a + b where after matching "b" the cursor has no next
const expression = createExpressionPattern();
const result = expression.exec("a + b");
expect(result.cursor.hasError).toBe(false);
expect(result.ast).not.toBeNull();
});
test("Postfix operator", () => {
const bang = new Sequence("factorial", [
new Reference("expression"),
new Literal("bang", "!"),
]);
const variable = new Literal("variable", "a");
const expression = new Expression("expression", [bang, variable]);
const result = expression.exec("a!");
expect(result.cursor.hasError).toBe(false);
expect(result.ast).not.toBeNull();
});
test("Chained postfix operators", () => {
const bang = new Sequence("factorial", [
new Reference("expression"),
new Literal("bang", "!"),
]);
const variable = new Literal("variable", "a");
const expression = new Expression("expression", [bang, variable]);
// a!! means: atom "a", then postfix "!" matched, cursor advances,
// another "!" matched, cursor has no next -> shouldStopParsing
const result = expression.exec("a!!");
expect(result.cursor.hasError).toBe(false);
});
test("Expression with no atoms returns null", () => {
// All patterns are infix (both ends have references), so no atoms exist
const infix = new Sequence("add", [
new Reference("expression"),
new Literal("plus", "+"),
new Reference("expression"),
]);
const expression = new Expression("expression", [infix]);
const result = expression.exec("a + b");
// No atom patterns -> parse returns null
expect(result.ast).toBeNull();
expect(result.cursor.hasError).toBe(true);
});
test("test() returns boolean", () => {
const expression = createExpressionPattern();
expect(expression.test("a + b")).toBe(true);
expect(expression.test("a + b * c")).toBe(true);
expect(expression.test("???")).toBe(false);
});
test("Accessor: atomPatterns returns atom patterns", () => {
const expression = createExpressionPattern();
expression.build();
// The expression has group and variables as atoms
expect(expression.atomPatterns.length).toBeGreaterThan(0);
});
test("Accessor: infixPatterns, prefixPatterns, postfixPatterns", () => {
const negate = new Sequence("negate", [
new Literal("minus", "-"),
new Reference("expression"),
]);
const bang = new Sequence("factorial", [
new Reference("expression"),
new Literal("bang", "!"),
]);
const add = new Sequence("add", [
new Reference("expression"),
new Literal("plus", "+"),
new Reference("expression"),
]);
const variable = new Literal("variable", "a");
const expression = new Expression("expression", [add, negate, bang, variable]);
expression.build();
expect(expression.prefixPatterns.length).toBe(1);
expect(expression.postfixPatterns.length).toBe(1);
expect(expression.infixPatterns.length).toBe(1);
expect(expression.atomPatterns.length).toBe(1);
});
test("Accessor: binaryPatterns is deprecated alias for infixPatterns", () => {
const expression = createExpressionPattern();
expression.build();
expect(expression.binaryPatterns).toBe(expression.infixPatterns);
});
test("Accessor: originalPatterns returns the original list", () => {
const a = new Literal("a", "a");
const b = new Literal("b", "b");
const expression = new Expression("expression", [a, b]);
expect(expression.originalPatterns).toContain(a);
expect(expression.originalPatterns).toContain(b);
expect(expression.originalPatterns.length).toBe(2);
});
test("getTokens returns atom and prefix tokens", () => {
const negate = new Sequence("negate", [
new Literal("minus", "-"),
new Reference("expression"),
]);
const variable = new Options("variable", [
new Literal("a", "a"),
new Literal("b", "b"),
]);
const expression = new Expression("expression", [negate, variable]);
const tokens = expression.getTokens();
expect(tokens).toContain("-");
expect(tokens).toContain("a");
expect(tokens).toContain("b");
});
test("getTokensAfter for prefix child returns atoms and prefix tokens", () => {
const negate = new Sequence("negate", [
new Literal("minus", "-"),
new Reference("expression"),
]);
const variable = new Literal("variable", "x");
const expression = new Expression("expression", [negate, variable]);
expression.build();
// The prefix pattern is the extracted prefix (minus only), get the actual child
const prefixChild = expression.prefixPatterns[0];
const tokens = expression.getTokensAfter(prefixChild as any);
// After a prefix, expect atom tokens (and prefix tokens for nested prefixes)
expect(tokens).toContain("-");
expect(tokens).toContain("x");
});
test("getTokensAfter for atom child returns postfix and infix tokens", () => {
const bang = new Sequence("factorial", [
new Reference("expression"),
new Literal("bang", "!"),
]);
const add = new Sequence("add", [
new Reference("expression"),
new Literal("plus", "+"),
new Reference("expression"),
]);
const variable = new Literal("variable", "x");
const expression = new Expression("expression", [add, bang, variable]);
expression.build();
const atomChild = expression.atomPatterns[0];
const tokens = expression.getTokensAfter(atomChild as any);
expect(tokens).toContain("!");
expect(tokens).toContain("+");
});
test("getTokensAfter for postfix child returns postfix and infix tokens", () => {
const bang = new Sequence("factorial", [
new Reference("expression"),
new Literal("bang", "!"),
]);
const add = new Sequence("add", [
new Reference("expression"),
new Literal("plus", "+"),
new Reference("expression"),
]);
const variable = new Literal("variable", "x");
const expression = new Expression("expression", [add, bang, variable]);
expression.build();
const postfixChild = expression.postfixPatterns[0];
const tokens = expression.getTokensAfter(postfixChild as any);
expect(tokens).toContain("!");
expect(tokens).toContain("+");
});
test("getTokensAfter for unknown child returns empty", () => {
const expression = createOptionsExpression();
expression.build();
const unrelated = new Literal("z", "z");
const tokens = expression.getTokensAfter(unrelated);
expect(tokens).toEqual([]);
});
test("getTokensAfter for atom child with parent includes parent next tokens", () => {
const variable = new Literal("variable", "x");
const expression = new Expression("expression", [variable]);
// Embed expression as child of a sequence to give it a parent
const wrapper = new Sequence("wrapper", [
expression,
new Literal("end", ";"),
]);
// The Sequence clones children, so get the actual wired-up expression
const wiredExpression = wrapper.children[0] as Expression;
wiredExpression.build();
const atomChild = wiredExpression.atomPatterns[0];
const tokens = wiredExpression.getTokensAfter(atomChild as any);
// The expression's parent is the wrapper Sequence.
// getTokensAfter for an atom calls this._parent.getNextTokens()
// which asks the wrapper's parent for what follows the wrapper.
// Since the wrapper has no grandparent, this returns [].
// But the branch (this._parent != null) IS taken.
expect(Array.isArray(tokens)).toBe(true);
});
test("getPatterns returns atom and prefix patterns", () => {
const negate = new Sequence("negate", [
new Literal("minus", "-"),
new Reference("expression"),
]);
const variable = new Literal("variable", "x");
const expression = new Expression("expression", [negate, variable]);
const patterns = expression.getPatterns();
expect(patterns.length).toBeGreaterThan(0);
});
test("getPatternsAfter for prefix child returns atom and prefix patterns", () => {
const negate = new Sequence("negate", [
new Literal("minus", "-"),
new Reference("expression"),
]);
const variable = new Literal("variable", "x");
const expression = new Expression("expression", [negate, variable]);
expression.build();
const prefixChild = expression.prefixPatterns[0];
const patterns = expression.getPatternsAfter(prefixChild as any);
expect(patterns.length).toBeGreaterThan(0);
});
test("getPatternsAfter for atom child returns postfix and infix patterns", () => {
const add = new Sequence("add", [
new Reference("expression"),
new Literal("plus", "+"),
new Reference("expression"),
]);
const variable = new Literal("variable", "x");
const expression = new Expression("expression", [add, variable]);
expression.build();
const atomChild = expression.atomPatterns[0];
const patterns = expression.getPatternsAfter(atomChild as any);
// Should include the infix delimiter pattern
expect(patterns.length).toBeGreaterThan(0);
});
test("getPatternsAfter for atom child with parent includes parent next patterns", () => {
const variable = new Literal("variable", "x");
const expression = new Expression("expression", [variable]);
const wrapper = new Sequence("wrapper", [
expression,
new Literal("end", ";"),
]);
const wiredExpression = wrapper.children[0] as Expression;
wiredExpression.build();
const atomChild = wiredExpression.atomPatterns[0];
const patterns = wiredExpression.getPatternsAfter(atomChild as any);
// The branch this._parent != null is taken. getNextPatterns on the
// wrapper returns [] since the wrapper has no grandparent, but the
// code path through line 577-578 is exercised.
expect(Array.isArray(patterns)).toBe(true);
});
test("getPatternsAfter for postfix child returns postfix and infix patterns", () => {
const bang = new Sequence("factorial", [
new Reference("expression"),
new Literal("bang", "!"),
]);
const add = new Sequence("add", [
new Reference("expression"),
new Literal("plus", "+"),
new Reference("expression"),
]);
const variable = new Literal("variable", "x");
const expression = new Expression("expression", [add, bang, variable]);
expression.build();
const postfixChild = expression.postfixPatterns[0];
const patterns = expression.getPatternsAfter(postfixChild as any);
expect(patterns.length).toBeGreaterThan(0);
});
test("getPatternsAfter for postfix child with parent includes parent next patterns", () => {
const bang = new Sequence("factorial", [
new Reference("expression"),
new Literal("bang", "!"),
]);
const variable = new Literal("variable", "x");
const expression = new Expression("expression", [bang, variable]);
const wrapper = new Sequence("wrapper", [
expression,
new Literal("end", ";"),
]);
const wiredExpression = wrapper.children[0] as Expression;
wiredExpression.build();
const postfixChild = wiredExpression.postfixPatterns[0];
const patterns = wiredExpression.getPatternsAfter(postfixChild as any);
// The branch this._parent != null is taken, exercising lines 593-594.
expect(Array.isArray(patterns)).toBe(true);
});
test("getNextTokens with no parent returns empty", () => {
const expression = createOptionsExpression();
expression.build();
expect(expression.getNextTokens()).toEqual([]);
});
test("getNextTokens with parent returns parent's tokens after", () => {
const variable = new Literal("variable", "x");
const expression = new Expression("expression", [variable]);
const wrapper = new Sequence("wrapper", [
expression,
new Literal("end", ";"),
]);
const wiredExpression = wrapper.children[0] as Expression;
wiredExpression.build();
const tokens = wiredExpression.getNextTokens();
expect(tokens).toContain(";");
});
test("getNextPatterns with no parent returns empty", () => {
const expression = createOptionsExpression();
expression.build();
expect(expression.getNextPatterns()).toEqual([]);
});
test("getNextPatterns with parent returns parent's patterns after", () => {
const variable = new Literal("variable", "x");
const expression = new Expression("expression", [variable]);
const wrapper = new Sequence("wrapper", [
expression,
new Literal("end", ";"),
]);
const wiredExpression = wrapper.children[0] as Expression;
wiredExpression.build();
const patterns = wiredExpression.getNextPatterns();
expect(patterns.length).toBeGreaterThan(0);
});
test("find delegates to findPattern", () => {
const expression = createOptionsExpression();
expression.build();
const found = expression.find(p => p.name === "a");
expect(found).not.toBeNull();
expect(found!.name).toBe("a");
const notFound = expression.find(p => p.name === "nonexistent");
expect(notFound).toBeNull();
});
test("Prefix matched but input ends before atom", () => {
// When a prefix matches but there's nothing left for the atom
const negate = new Sequence("negate", [
new Literal("minus", "-"),
new Reference("expression"),
]);
const variable = new Literal("variable", "a");
const expression = new Expression("expression", [negate, variable]);
// Just "-" with no atom to follow: prefix matches "-", cursor has no next
const result = expression.exec("-");
// This should fail because there's no atom after the prefix
expect(result.ast).toBeNull();
});
test("Atom-only expression with trailing input stops at binary check", () => {
// "a" matches as atom, cursor still has next ("b" remains), no postfix matches,
// _tryToMatchBinary is entered, infixPatterns.length === 0 -> shouldStopParsing
const a = new Literal("a", "a");
const expression = new Expression("expression", [a]);
// Input "ab": atom "a" matches index 0, cursor advances to index 1 ("b"),
// no postfix/infix patterns -> shouldStopParsing in _tryToMatchBinary.
// exec returns null ast since "b" is leftover, but the code path is exercised.
const result = expression.exec("ab");
expect(result.cursor.hasError).toBe(true);
});
test("getTokensAfter for postfix child with parent includes parent tokens", () => {
const bang = new Sequence("factorial", [
new Reference("expression"),
new Literal("bang", "!"),
]);
const variable = new Literal("variable", "x");
const expression = new Expression("expression", [bang, variable]);
const wrapper = new Sequence("wrapper", [
expression,
new Literal("end", ";"),
]);
const wiredExpression = wrapper.children[0] as Expression;
wiredExpression.build();
const postfixChild = wiredExpression.postfixPatterns[0];
const tokens = wiredExpression.getTokensAfter(postfixChild as any);
// Exercises the postfix branch with parent (line 538-539)
expect(Array.isArray(tokens)).toBe(true);
});
test("getPatternsAfter for unknown child returns empty", () => {
const expression = createOptionsExpression();
expression.build();
const unrelated = new Literal("z", "z");
const patterns = expression.getPatternsAfter(unrelated);
expect(patterns).toEqual([]);
});
test("isEqual compares two expressions", () => {
const a = new Literal("a", "a");
const b = new Literal("b", "b");
const expr1 = new Expression("expression", [a, b]);
const expr2 = expr1.clone();
expr1.build();
(expr2 as Expression).build();
expect(expr1.isEqual(expr2 as Expression)).toBe(true);
});
test("right-associated binary nests right with a tighter operator between operators", () => {
// Regression for a jq-style binder: `add` is tighter than a
// right-associated `as`. In `a as b + c as d` the `b + c` binds first,
// leaving `a as (b+c) as d`, which must nest right into
// `a as ((b + c) as d)` — NOT left into `(a as (b + c)) as d`.
const variables = new Options("variables", [
new Literal("a", "a"),
new Literal("b", "b"),
new Literal("c", "c"),
new Literal("d", "d"),
]);
const addExpression = new Sequence("add-expression", [
new Reference("expression"),
new Literal("add", " + "),
new Reference("expression"),
]);
const asExpression = new Sequence("as-expression", [
new Reference("expression"),
new Literal("as", " as "),
new Reference("expression"),
]);
const expression = new Expression("expression", [
addExpression,
new RightAssociated(asExpression),
variables,
]);
const { ast } = expression.exec("a as b + c as d");
expect(ast).not.toBeNull();
expect(ast?.name).toBe("as-expression");
// Left operand of the outer `as` is the bare atom `a` (right-nested).
expect(ast?.children[0].name).toBe("a");
const inner = ast?.children[ast.children.length - 1];
expect(inner?.name).toBe("as-expression");
// The inner `as`'s left operand is the `(b + c)` sub-expression.
expect(inner?.children[0].name).toBe("add-expression");
expect(inner?.children[inner.children.length - 1].name).toBe("d");
});
});