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}