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