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