import {LocationRange} from 'pegjs'; import {allOperators, Token, TokenResult} from './parser'; /** * Used internally within treeResultLocator to stop recursion once we've * located a matched result. */ class TokenResultFound extends Error { result: any; constructor(result: any) { super(); this.result = result; } } /** * Used as the marker to skip token traversal in treeResultLocator */ const skipTokenMarker = Symbol('Returned to skip visiting a token'); type VisitorFn = (opts: { /** * Call this to return the provided value as the result of treeResultLocator */ returnResult: (result: any) => TokenResultFound; /** * Return this to skip visiting any inner tokens */ skipToken: typeof skipTokenMarker; /** * The token being visited */ token: TokenResult; }) => null | TokenResultFound | typeof skipTokenMarker; type TreeResultLocatorOpts = { /** * The value to return when returnValue was never called and all nodes of the * search tree were visited. */ noResultValue: any; /** * The tree to visit */ tree: TokenResult[]; /** * A function used to check if we've found the result in the node we're * visiting. May also indicate that we want to skip any further traversal of * inner nodes. */ visitorTest: VisitorFn; }; /** * Utility function to visit every Token node within an AST tree (in DFS order) * and apply a test method that may choose to return some value from that node. * * You must call the `returnValue` method for a result to be returned. * * When returnValue is never called and all nodes of the search tree have been * visited the noResultValue will be returned. */ export function treeResultLocator({ tree, visitorTest, noResultValue, }: TreeResultLocatorOpts): T { const returnResult = (result: any) => new TokenResultFound(result); const nodeVisitor = (token: TokenResult | null) => { if (token === null) { return; } const result = visitorTest({token, returnResult, skipToken: skipTokenMarker}); // Bubble the result back up. // // XXX: Using a throw here is a bit easier than threading the return value // back up through the recursive call tree. if (result instanceof TokenResultFound) { throw result; } // Don't traverse into any nested tokens if (result === skipTokenMarker) { return; } switch (token.type) { case Token.Filter: nodeVisitor(token.key); nodeVisitor(token.value); break; case Token.KeyExplicitTag: nodeVisitor(token.key); break; case Token.KeyAggregate: nodeVisitor(token.name); token.args && nodeVisitor(token.args); nodeVisitor(token.argsSpaceBefore); nodeVisitor(token.argsSpaceAfter); break; case Token.LogicGroup: token.inner.forEach(nodeVisitor); break; case Token.KeyAggregateArgs: token.args.forEach(v => nodeVisitor(v.value)); break; case Token.ValueNumberList: case Token.ValueTextList: token.items.forEach((v: any) => nodeVisitor(v.value)); break; default: } }; try { tree.forEach(nodeVisitor); } catch (error) { if (error instanceof TokenResultFound) { return error.result; } throw error; } return noResultValue; } type TreeTransformerOpts = { /** * The function used to transform each node */ transform: (token: TokenResult) => any; /** * The tree to transform */ tree: TokenResult[]; }; /** * Utility function to visit every Token node within an AST tree and apply * a transform to those nodes. */ export function treeTransformer({tree, transform}: TreeTransformerOpts) { const nodeVisitor = (token: TokenResult | null) => { if (token === null) { return null; } switch (token.type) { case Token.Filter: return transform({ ...token, key: nodeVisitor(token.key), value: nodeVisitor(token.value), }); case Token.KeyExplicitTag: return transform({ ...token, key: nodeVisitor(token.key), }); case Token.KeyAggregate: return transform({ ...token, name: nodeVisitor(token.name), args: token.args ? nodeVisitor(token.args) : token.args, argsSpaceBefore: nodeVisitor(token.argsSpaceBefore), argsSpaceAfter: nodeVisitor(token.argsSpaceAfter), }); case Token.LogicGroup: return transform({ ...token, inner: token.inner.map(nodeVisitor), }); case Token.KeyAggregateArgs: return transform({ ...token, args: token.args.map(v => ({...v, value: nodeVisitor(v.value)})), }); case Token.ValueNumberList: case Token.ValueTextList: return transform({ ...token, // TODO(ts): Not sure why `v` cannot be inferred here items: token.items.map((v: any) => ({...v, value: nodeVisitor(v.value)})), }); default: return transform(token); } }; return tree.map(nodeVisitor); } type GetKeyNameOpts = { /** * Include arguments in aggregate key names */ aggregateWithArgs?: boolean; }; /** * Utility to get the string name of any type of key. */ export const getKeyName = ( key: TokenResult, options: GetKeyNameOpts = {} ) => { const {aggregateWithArgs} = options; switch (key.type) { case Token.KeySimple: return key.value; case Token.KeyExplicitTag: return key.key.value; case Token.KeyAggregate: return aggregateWithArgs ? `${key.name.value}(${key.args ? key.args.text : ''})` : key.name.value; default: return ''; } }; export function isWithinToken( node: {location: LocationRange} | null | undefined, position: number ) { if (!node) { return false; } return position >= node.location.start.offset && position <= node.location.end.offset; } export function isOperator(value: string) { return allOperators.some(op => op === value); }