UNPKG

clarity-pattern-parser

Version:

Parsing Library for Typescript and Javascript.

715 lines (579 loc) 25.6 kB
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"); }); });