UNPKG

jora

Version:

JavaScript object query engine

347 lines (296 loc) 11 kB
import { hasOwn } from '../utils/misc.js'; import createError from './error.js'; import { compile as nodes } from './nodes/index.js'; const IdStartRx = /^[\p{ID_Start}$_]$/u; const IdContinueRx = /^[\p{ID_Continue}$]$/u; export function compile(ast, tolerant, suggestions) { return compileInternal(ast, 'query', tolerant, suggestions); } export function compileMethod(ast, tolerant, suggestions) { return compileInternal(ast, 'method', tolerant, suggestions); } class Scope extends Set { constructor(items, awaitInit = [], arg1 = false, $ref = '$') { super(items); this.own = new Set(); this.awaitInit = new Set(awaitInit); this.firstCurrent = null; this.captureCurrent = []; this.arg1 = arg1; this.$ref = $ref; } spawn(arg1, $ref) { return new Scope(this, this.awaitInit, arg1, $ref); } } function compileInternal(ast, kind, tolerant = false, suggestions = null) { function newStatPoint(values) { const spName = 's' + spNames.length; spNames.push(values ? [spName, values] : [spName]); return spName; } function getNodeSpName(node) { let spName = nodeSpName.get(node); if (!spName) { nodeSpName.set(node, spName = newStatPoint()); } return spName; } function addSuggestPoint(start, end, type, spName, related) { let range = [start, end, JSON.stringify(type)]; if (type === 'var') { if (!ctx.scope.size) { return; } range.push(JSON.stringify([...ctx.scope])); } else { if (!spName) { spName = newStatPoint(); } range.push(spName); if (related) { range.push(typeof related === 'string' ? related : getNodeSpName(related)); } } normalizedSuggestRanges.push(range); return spName; } function createScope(fn, defCurrent, $ref = '$') { const prevScope = ctx.scope; const scopeStart = buffer.length - 1; const arg1 = prevScope.arg1 || (kind === 'method' && 'arguments[1]'); ctx.scope = prevScope.spawn(arg1, $ref); fn(); if (ctx.scope.captureCurrent.length) { const spName = ctx.scope.captureCurrent.reduce( (spName, range) => addSuggestPoint(...range, spName), undefined ); const stat = 'stat(' + spName + ',' + ctx.scope.$ref + ')'; if (ctx.scope.firstCurrent) { buffer[ctx.scope.firstCurrent] = stat; } else { buffer[scopeStart] += defCurrent(stat); } } ctx.scope = prevScope; } function walk(node, relatedNode) { let spName = false; if (suggestions !== null) { if (suggestions.has(node)) { for (const [start, end, type, related] of suggestions.get(node)) { if (type === 'var') { addSuggestPoint(start, end, type); } else if (related === true) { ctx.scope.captureCurrent.push([start, end, type]); } else { if (!spName) { spName = getNodeSpName(node); buffer.push('stat(' + spName + ','); } if (type) { addSuggestPoint(start, end, type, spName, related); } } } } if (node.type === 'Current' && ctx.scope.firstCurrent === null) { ctx.scope.firstCurrent = buffer.length; } } if (nodes.has(node.type)) { nodes.get(node.type)(node, ctx, relatedNode); } else { throw new Error('Unknown node type "' + node.type + '"'); } if (spName) { buffer.push(')'); } } const spNames = []; const nodeSpName = new WeakMap(); const allocatedVars = []; const normalizedSuggestRanges = []; const buffer = [ { toString() { if (kind === 'query') { return [ '((data,context)=>{', ...ctx.usedMethods.size || ctx.usedAssertions.size ? [ 'const method = (name,...args)=>m[name].apply(mctx,args);', 'const assertion = (name,...args)=>a[name].apply(mctx,args);', 'const mctx = Object.freeze({context,method,assertion});' ] : [] ].join('\n') + '\n'; } if (kind === 'method') { return [ '(function(data){', 'const mctx = this;', 'const context = mctx.context;' ].join('\n') + '\n'; } throw new Error('Unknown kind: ' + kind); } }, { toString() { return allocatedVars.length > 0 ? 'let ' + allocatedVars + ';\n' : ''; } }, { toString() { return spNames.length > 0 ? [ 'const stat=(s,v)=>(s.add(v),v);\n', 'const ' + spNames.map(([name, values]) => name + '=new Set(' + (values ? JSON.stringify(values) : '') + ')' ) + ';\n' ].join('') : ''; } }, { toString() { const lists = suggestions && Array.isArray(suggestions.literalList) && suggestions.literalList .map(([name, values]) => name + '=' + JSON.stringify(values)); return lists && lists.length ? 'const ' + lists + ';\n' : ''; } }, suggestions === null ? 'return ' : 'return{\nvalue: ' ]; const initCtx = {}; const usedBuildinMethods = new Set(); const ctx = { tolerant, usedAssertions: new Map(), usedMethods: new Map(), suggestions: suggestions !== null, buildinFn(name) { usedBuildinMethods.add(name); return 'f.' + name; }, scope: new Scope(), createScope, error(message, node) { const error = new SyntaxError(message); if (node && node.range) { error.details = { loc: { range: node.range } }; } if (!tolerant) { throw error; } return error; }, allocateVar() { const name = 'tmp' + allocatedVars.length; allocatedVars.push(name); return name; }, unescapeName(name, node, ident = true) { return name.replace(/\\u[0-9a-fA-F]{4}/g, (m, offset) => { const unescapedChar = String.fromCharCode(parseInt(m.slice(2), 16)); const rx = offset === 0 ? IdStartRx : IdContinueRx; if (ident && !rx.test(unescapedChar)) { this.error( `Bad escape sequence in indentifier name (expected ${rx})`, node && { range: [ node.range[0] + offset, node.range[0] + offset + 6 ] } ); } return unescapedChar; }); }, put: chunk => buffer.push(chunk), putIdent(name, node) { if (!/^[\p{ID_Start}$_][\p{ID_Continue}$]*$/u.test(name)) { throw this.error('Bad identifier name ' + JSON.stringify(name), node); } this.put(name); }, node: walk, nodeOrCurrent(node, relatedNode) { walk(node || { type: 'Current' }, relatedNode); }, list(list, sep, relatedNode) { list.forEach((node, idx) => { if (idx > 0) { buffer.push(sep); } walk(node, relatedNode); }); } }; createScope( () => walk(ast), (sp) => { buffer.push(')'); return '(' + sp + ','; }, 'data' ); if (!tolerant) { const { usedMethods, usedAssertions } = ctx; if (usedAssertions.size) { buffer.unshift(' this.assertAssertions(a)||'); initCtx.assertAssertions = function(providedAssertions) { for (const [assertion, range] of usedAssertions.entries()) { if (!hasOwn(providedAssertions, assertion)) { return () => { throw Object.assign( new Error(`Assertion "${assertion}" is not defined`), { details: { loc: { range } } } ); }; } } }; } if (usedMethods.size) { buffer.unshift(' this.assertMethods(m)||'); initCtx.assertMethods = function(providedMethods) { for (const [method, ranges] of usedMethods.entries()) { if (!hasOwn(providedMethods, method)) { return () => { throw Object.assign( new Error( `Method "${method}" is not defined. If that's a custom method ` + 'make sure you added it with "methods" section in options' ), { details: { loc: { range: ranges[0] } } } ); }; } } }; } } if (suggestions !== null) { buffer.push( ',\nstats: [' + normalizedSuggestRanges.map(s => '[' + s + ']') + ']' + ',\nmethods: m' + ',\nassertions: a' + '\n}'); } return compileFunction(initCtx, 'return' + buffer.join('') + '})'); }; function compileFunction(initCtx, functionBody) { try { const fn = new Function('f,m,a', functionBody); return Object.assign(fn.bind(initCtx), { toString() { return fn.toString().replace(/^(\S+\s+)anonymous([^)\s]+)\s*\)/, '$1query$2)'); } }); } catch (e) { // console.log('Jora query compilation error', buffer.join('')) // process.exit(); throw createError('SyntaxError', 'Jora query compilation error', { compiledSource: `function(f,m,a){\n${functionBody}\n}`, details: e }); } }