UNPKG

rd-parse-jsexpr

Version:

ES6 expression grammar for use with rd-parse parser generator

215 lines (158 loc) 9.54 kB
import { Ignore, All, Any, Optional, Star, Node } from 'rd-parse'; // An "immutable" pure functional reduction of ECMAScript grammar: // loosely based on https://gist.github.com/avdg/1f10e268e484b1284b46 // and http://tomcopeland.blogs.com/EcmaScript.html // Matches (almost) anything you can put on the right hand side of an assignment operator in ES6 // Tokens: mostly from https://www.regular-expressions.info/examplesprogrammer.html export const IgnoreWhitespace = Rule => Ignore(/^\s+/, Rule); export const StringToken = Any( /^('[^'\\]*(?:\\.[^'\\]*)*')/, // single-quoted /^("[^"\\]*(?:\\.[^"\\]*)*")/, // double-quoted ); // Turn off ignore whitespace for InterpolationChunk export const InterpolationChunkToken = /^((?:\$(?!{)|\\.|[^`$\\])+)/; export const NumericToken = Any( /^((?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][-+]?[0-9]+)?)\b/, // decimal /^(0[xX][0-9a-fA-F]+)\b/ // hex ); export const NullToken = /^(null)\b/; export const BooleanToken = /^(true|false)\b/; // const RegExToken = /^\/([^/]+)\/([gimuy]*\b)?/; export const IdentifierToken = /^([a-zA-Z_$][a-zA-Z0-9_$]*)/; const srcMap = (obj, $, $next) => Object.defineProperties(obj, { pos: { writable: true, configurable: true, value: $.pos }, text: { writable: true, configurable: true, value: ($.text || $next.text).slice($.pos, $next.pos) }, }); const withSrcMap = (reducer = ([n]) => n) => (parts, ...$$) => srcMap(reducer(parts, ...$$), ...$$); const Expression = $ => Grammar($); const Identifier = Node(IdentifierToken, ([name]) => ({ type: 'Identifier', name })); // Literals const StringLiteral = Node(StringToken, ([raw]) => ({ type: 'Literal', value: eval(raw), raw })); const NumericLiteral = Node(NumericToken, ([raw]) => ({ type: 'Literal', value: +raw, raw })); const NullLiteral = Node(NullToken, ([raw]) => ({ type: 'Literal', value: null, raw })); const BooleanLiteral = Node(BooleanToken, ([raw]) => ({ type: 'Literal', value: raw === 'true', raw })); // const RegExLiteral = Node(RegExToken, ([raw, flags]) => ({ type: 'Literal', value: new RegExp(raw, flags), raw: `/${raw}/${flags||''}` })); const InterpolationChunk = Node(InterpolationChunkToken, ([raw]) => ['chunks', eval('`' + raw + '`')]); const TemplateInlineExpression = Node(All('${', IgnoreWhitespace(Expression), '}'), ([expression]) => ['expressions', expression]); const TemplateLiteral = Node(Ignore(null, All('`', Star(Any(InterpolationChunk, TemplateInlineExpression)), '`')), parts => ({ type: 'TemplateLiteral', parts })); const Literal = Any(StringLiteral, NumericLiteral, NullLiteral, BooleanLiteral, TemplateLiteral /*, RegExLiteral*/); // Array literal const EmptyElement = Node(',', () => ({ empty: true })); const Elision = All(',', Star(EmptyElement)); const SpreadElement = Node(All('...', Expression), ([spread]) => ({ spread })); const Element = Any(SpreadElement, Expression); const ElementList = All(Star(EmptyElement), Element, Star(All(Elision, Element))); const ArrayLiteral = Node(All('[', Any( All(Star(EmptyElement), ']'), All(ElementList, Optional(Elision), ']'), )), elements => ({ type: 'ArrayLiteral', elements })); // Compound expression const CompoundExpression = Node(All(Expression, Star(All(',', Expression))), leafs => leafs.length > 1 ? { type: 'CompoundExpression', leafs } : leafs[0]); // Object literal const ShortNotation = Node(Identifier, withSrcMap(([expr]) => ({ ...expr, shortNotation: true }))); const ComputedPropertyName = Node(All('[', CompoundExpression, ']'), ([computed]) => ({ computed })); const PropertyName = Any(IdentifierToken, StringLiteral, NumericLiteral, ComputedPropertyName); const PropertyDefinition = Node(Any(All(PropertyName, ':', Expression), ShortNotation, SpreadElement), ([name, value]) => name.spread ? name : ({ name: value ? name : name.name, value: value || name }) ); const PropertyDefinitions = All(PropertyDefinition, Star(All(',', PropertyDefinition))); const PropertyDefinitionList = Optional( All(PropertyDefinitions, Optional(',')) ); const ObjectLiteral = Node(All('{', PropertyDefinitionList, '}'), properties => ({ type: 'ObjectLiteral', properties})); // Primary expression const PrimaryExpression = Node(Any(Literal, Identifier, ArrayLiteral, ObjectLiteral, All('(', CompoundExpression, ')')), withSrcMap()); // Member expression const ArgumentsList = All(Element, Star(All(',', Element))); const Arguments = Node(All('(', Optional(All(ArgumentsList, Optional(','))), ')'), args => ({ args })); const PropertyAccess = Any(All('.', IdentifierToken), ComputedPropertyName); const PropertyAccessOrArguments = Node(Any(PropertyAccess, Arguments), ([part], _, $next) => ({ part, $next })); const MemberExpression = Node(All(Optional(/^new\b/), PrimaryExpression, Star(PropertyAccessOrArguments)), (parts, $, $last) => { const result = parts.reduce((acc, { part, $next }) => { if (part.args) { return srcMap($.pos !== acc.pos ? { type: 'NewExpression', ctor: acc, arguments: part.args } : { type: 'CallExpression', callee: acc, arguments: part.args }, $, $next); } return srcMap({ type: 'MemberExpression', object: acc, property: part }, { pos: acc.pos }, $next); }); if (result.pos === $.pos) return result; return srcMap({ type: 'NewExpression', ctor: result, arguments: [] }, $, $last); } ); // Unary expressions const Operator = Rule => Node(Rule, (_, $, $next) => ({ $, operator: $.text.substring($.pos, $next.pos) })); const UnaryOperator = Operator(Any('+', '-', '~', '!', /^typeof\b/)); const UnaryExpression = Node(All(Star(UnaryOperator), MemberExpression), (parts, _, $next) => parts.reduceRight((argument, { $, operator }) => srcMap({ type: 'UnaryExpression', argument, operator }, $, $next))); // Binary expressions const BitwiseAnd = /^&(?!&)/; const BitwiseOr = /^\|(?!\|)/; const BinaryOperatorPrecedence = [ '**', Any('*', '/', '%'), Any('+', '-'), Any('>>>', '<<', '>>'), Any('<=', '>=', '<', '>', /^instanceof\b/, /^in\b/), Any('===', '!==', '==', '!='), BitwiseAnd, '^', BitwiseOr, '&&', '||' ]; const associativity = BinaryOp => BinaryOp === '**' ? rightToLeft : leftToRight; function leftToRight(parts, $) { let left = parts[0]; for (let i = 1; i < parts.length; i += 2) { const [operator, right] = [parts[i].operator, parts[i + 1]]; left = srcMap({ type: 'BinaryExpression', left, operator, right }, $, { pos: right.pos + right.text.length }); } return left; } function rightToLeft(parts, _, $next) { let right = parts[parts.length - 1]; for (let i = parts.length - 2; i >= 0; i -= 2) { const [left, operator] = [parts[i - 1], parts[i].operator]; right = srcMap({ type: 'BinaryExpression', left, operator, right }, { pos: left.pos }, $next); } return right; } const ExpressionConstructor = (Expr, BinaryOp) => Node(All(Expr, Star(All(Operator(BinaryOp), Expr))), associativity(BinaryOp)); const LogicalORExpression = BinaryOperatorPrecedence.reduce(ExpressionConstructor, UnaryExpression); const ConditionalExpression = Node(All(LogicalORExpression, Optional(All('?', Expression, ':', Expression))), ([test, consequent, alternate]) => consequent ? ({ type: 'ConditionalExpression', test, consequent, alternate }) : test); // Binding patterns export const BindingElement = $ => BindingElementImpl($); const BoundName = Node(IdentifierToken, withSrcMap(([name]) => ({ bindingType: 'SingleName', name }))); const RestElement = Node(All('...', BoundName), ([rest]) => ({rest})); const BindingList = (List, bindingType) => Node(Any(All(Node(List, bound => ({ bound })), Optional(All(',', Optional(RestElement)))), Optional(RestElement)), parts => parts.reduce((acc, part) => Object.assign(acc, part), { bound: [], bindingType })); const WithInitializer = Pattern => Node(All(Pattern, Optional(All('=', Expression))), ([pattern, initializer]) => initializer ? { pattern, initializer } : { pattern }); const SingleNameBinding = WithInitializer(BoundName); const BindingProperty = Node(Any(All(PropertyName, ':', BindingElement), SingleNameBinding), ([prop, pattern]) => pattern ? { prop, ...pattern } : { prop: prop.pattern.name, ...prop }); const BindingPropertyList = All(BindingProperty, Star(All(',', BindingProperty))); const ObjectBindingPattern = All('{', BindingList(BindingPropertyList, 'ObjectPattern'), '}'); const BindingElementList = All(Star(EmptyElement), Optional(All(BindingElement, Star(All(Elision, BindingElement))))); const ArrayBindingPattern = All('[', BindingList(BindingElementList, 'ArrayPattern'), ']'); export const BindingPattern = Node(Any(BoundName, ObjectBindingPattern, ArrayBindingPattern), withSrcMap()); const BindingElementImpl = Node(WithInitializer(BindingPattern), withSrcMap()); // Arrow functions const FormalsList = All(BindingElement, Star(All(',', BindingElement))); const FormalParameters = All('(', BindingList(FormalsList, 'FormalParameters'), ')'); const ArrowParameters = Any(BoundName, FormalParameters); const FoolSafe = Node('{', () => { throw new Error('Object literal returned from the arrow function needs to be enclosed in ()'); }); const ArrowResult = Any(FoolSafe, Expression); const ArrowFunction = Node(All(ArrowParameters, '=>', ArrowResult), ([parameters, result]) => ({ type: 'ArrowFunction', parameters, result })); const Grammar = Node(Any(ArrowFunction, ConditionalExpression), withSrcMap()); export default IgnoreWhitespace(Grammar);