Mirror: The magical sticky regex-based parser generator 馃
at v1.0.1 6.8 kB view raw
1import { parse } from '../parser'; 2import { SharedIds } from './sharedIds'; 3import { initGenerator, RootNode } from './generator'; 4 5export function makeHelpers(t) { 6 const importSourceRe = /reghex$|^reghex\/macro/; 7 const importName = 'reghex'; 8 const ids = new SharedIds(t); 9 initGenerator(ids, t); 10 11 let _hasUpdatedImport = false; 12 13 return { 14 /** Adds the reghex import declaration to the Program scope */ 15 updateImport(path) { 16 if (_hasUpdatedImport) return; 17 if (!importSourceRe.test(path.node.source.value)) return; 18 _hasUpdatedImport = true; 19 20 const defaultSpecifierIndex = path.node.specifiers.findIndex((node) => { 21 return t.isImportDefaultSpecifier(node); 22 }); 23 24 if (defaultSpecifierIndex > -1) { 25 path.node.specifiers.splice(defaultSpecifierIndex, 1); 26 } 27 28 if (path.node.source.value !== importName) { 29 path.node.source = t.stringLiteral(importName); 30 } 31 32 path.node.specifiers.push( 33 t.importSpecifier( 34 (ids.execId = path.scope.generateUidIdentifier('exec')), 35 t.identifier('_exec') 36 ), 37 t.importSpecifier( 38 (ids.patternId = path.scope.generateUidIdentifier('pattern')), 39 t.identifier('_pattern') 40 ) 41 ); 42 43 const tagImport = path.node.specifiers.find((node) => { 44 return t.isImportSpecifier(node) && node.imported.name === 'tag'; 45 }); 46 if (!tagImport) { 47 path.node.specifiers.push( 48 t.importSpecifier( 49 (ids.tagId = path.scope.generateUidIdentifier('tag')), 50 t.identifier('tag') 51 ) 52 ); 53 } else { 54 ids.tagId = tagImport.imported; 55 } 56 }, 57 58 /** Determines whether the given tagged template expression is a reghex match */ 59 isMatch(path) { 60 if ( 61 t.isTaggedTemplateExpression(path.node) && 62 t.isCallExpression(path.node.tag) && 63 t.isIdentifier(path.node.tag.callee) && 64 path.scope.hasBinding(path.node.tag.callee.name) 65 ) { 66 if (t.isVariableDeclarator(path.parentPath)) 67 path.parentPath._isMatch = true; 68 return true; 69 } 70 71 return ( 72 t.isVariableDeclarator(path.parentPath) && path.parentPath._isMatch 73 ); 74 }, 75 76 /** Given a reghex match, returns the path to reghex's match import declaration */ 77 getMatchImport(path) { 78 t.assertTaggedTemplateExpression(path.node); 79 const binding = path.scope.getBinding(path.node.tag.callee.name); 80 81 if ( 82 binding.kind !== 'module' || 83 !t.isImportDeclaration(binding.path.parent) || 84 !importSourceRe.test(binding.path.parent.source.value) || 85 !t.isImportDefaultSpecifier(binding.path.node) 86 ) { 87 return null; 88 } 89 90 return binding.path.parentPath; 91 }, 92 93 /** Given a match, returns an evaluated name or a best guess */ 94 getMatchName(path) { 95 t.assertTaggedTemplateExpression(path.node); 96 const nameArgumentPath = path.get('tag.arguments.0'); 97 const { confident, value } = nameArgumentPath.evaluate(); 98 if (!confident && t.isIdentifier(nameArgumentPath.node)) { 99 return nameArgumentPath.node.name; 100 } else if (confident && typeof value === 'string') { 101 return value; 102 } else { 103 return path.scope.generateUidIdentifierBasedOnNode(path.node); 104 } 105 }, 106 107 /** Given a match, hoists its expressions in front of the match's statement */ 108 _hoistExpressions(path) { 109 t.assertTaggedTemplateExpression(path.node); 110 111 const variableDeclarators = []; 112 const matchName = this.getMatchName(path); 113 114 const hoistedExpressions = path.node.quasi.expressions.map( 115 (expression) => { 116 if ( 117 t.isIdentifier(expression) && 118 path.scope.hasBinding(expression.name) 119 ) { 120 const binding = path.scope.getBinding(expression.name); 121 if (t.isVariableDeclarator(binding.path.node)) { 122 const matchPath = binding.path.get('init'); 123 if (this.isMatch(matchPath)) return expression; 124 } 125 } 126 127 const id = path.scope.generateUidIdentifier( 128 `${matchName}_expression` 129 ); 130 variableDeclarators.push( 131 t.variableDeclarator( 132 id, 133 t.callExpression(ids.pattern, [expression]) 134 ) 135 ); 136 return id; 137 } 138 ); 139 140 if (variableDeclarators.length) { 141 path 142 .getStatementParent() 143 .insertBefore(t.variableDeclaration('var', variableDeclarators)); 144 } 145 146 return hoistedExpressions; 147 }, 148 149 _hoistTransform(path) { 150 const transformNode = path.node.tag.arguments[1]; 151 if (!transformNode) return null; 152 if (t.isIdentifier(transformNode)) return transformNode; 153 154 const matchName = this.getMatchName(path); 155 const id = path.scope.generateUidIdentifier(`${matchName}_transform`); 156 const declarator = t.variableDeclarator(id, transformNode); 157 158 path 159 .getStatementParent() 160 .insertBefore(t.variableDeclaration('var', [declarator])); 161 return id; 162 }, 163 164 transformMatch(path) { 165 if (!path.node.tag.arguments.length) { 166 throw path 167 .get('tag') 168 .buildCodeFrameError( 169 'match() must at least be called with a node name' 170 ); 171 } 172 173 const matchName = this.getMatchName(path); 174 const nameNode = path.node.tag.arguments[0]; 175 const quasis = path.node.quasi.quasis.map((x) => x.value.cooked); 176 177 // Hoist expressions and wrap them in an execPattern call 178 const expressions = this._hoistExpressions(path).map((id) => { 179 // Directly call expression if it's sure to be another matcher 180 const binding = path.scope.getBinding(id.name); 181 if (binding && t.isVariableDeclarator(binding.path.node)) { 182 const matchPath = binding.path.get('init'); 183 if (this.isMatch(matchPath)) { 184 return t.callExpression(id, [ids.state]); 185 } 186 } 187 188 return t.callExpression(ids.exec, [ids.state, id]); 189 }); 190 191 // Hoist transform argument if necessary 192 const transformNode = this._hoistTransform(path); 193 194 let ast; 195 try { 196 ast = parse(quasis, expressions); 197 } catch (error) { 198 if (error.name !== 'SyntaxError') throw error; 199 throw path.get('quasi').buildCodeFrameError(error.message); 200 } 201 202 const generator = new RootNode(ast, nameNode, transformNode); 203 const body = t.blockStatement(generator.statements()); 204 const matchFunctionId = path.scope.generateUidIdentifier(matchName); 205 const matchFunction = t.functionExpression( 206 matchFunctionId, 207 [ids.state], 208 body 209 ); 210 path.replaceWith(matchFunction); 211 }, 212 }; 213}