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.isArrowFunctionExpression(expression) &&
111 t.isIdentifier(expression.body)
112 ) {
113 expression = expression.body;
114 } else if (
115 (t.isFunctionExpression(expression) ||
116 t.isArrowFunctionExpression(expression)) &&
117 t.isBlockStatement(expression.body) &&
118 expression.body.body.length === 1 &&
119 t.isReturnStatement(expression.body.body[0]) &&
120 t.isIdentifier(expression.body.body[0].argument)
121 ) {
122 expression = expression.body.body[0].argument;
123 }
124
125 if (
126 t.isIdentifier(expression) &&
127 path.scope.hasBinding(expression.name)
128 ) {
129 const binding = path.scope.getBinding(expression.name);
130 if (t.isVariableDeclarator(binding.path.node)) {
131 const matchPath = binding.path.get('init');
132 if (this.isMatch(matchPath)) return expression;
133 }
134 }
135
136 const id = path.scope.generateUidIdentifier(
137 `${matchName}_expression`
138 );
139
140 variableDeclarators.push(
141 t.variableDeclarator(
142 id,
143 t.callExpression(ids.pattern, [expression])
144 )
145 );
146
147 return id;
148 }
149 );
150
151 if (variableDeclarators.length) {
152 path
153 .getStatementParent()
154 .insertBefore(t.variableDeclaration('var', variableDeclarators));
155 }
156
157 return hoistedExpressions.map((id) => {
158 const binding = path.scope.getBinding(id.name);
159 if (binding && t.isVariableDeclarator(binding.path.node)) {
160 const matchPath = binding.path.get('init');
161 if (this.isMatch(matchPath)) return `${id.name}(state)`;
162 }
163
164 const input = t.isStringLiteral(id)
165 ? JSON.stringify(id.value)
166 : id.name;
167 return `${ids.exec.name}(state, ${input})`;
168 });
169 },
170
171 _prepareTransform(path) {
172 const transformNode = path.node.tag.arguments[1];
173
174 if (!transformNode) return null;
175 if (t.isIdentifier(transformNode)) return transformNode.name;
176
177 const matchName = this.getMatchName(path);
178 const id = path.scope.generateUidIdentifier(`${matchName}_transform`);
179 const declarator = t.variableDeclarator(id, transformNode);
180
181 path
182 .getStatementParent()
183 .insertBefore(t.variableDeclaration('var', [declarator]));
184
185 return id.name;
186 },
187
188 minifyMatch(path) {
189 if (!path.node.tag.arguments.length) {
190 throw path
191 .get('tag')
192 .buildCodeFrameError(
193 'match() must at least be called with a node name'
194 );
195 }
196
197 const quasis = path.node.quasi.quasis.map((x) =>
198 t.stringLiteral(x.value.cooked.replace(/\s*/g, ''))
199 );
200 const expressions = path.node.quasi.expressions;
201 const transform = this._prepareTransform(path);
202
203 path.replaceWith(
204 t.callExpression(path.node.tag, [
205 t.arrayExpression(quasis),
206 ...expressions,
207 ])
208 );
209 },
210
211 transformMatch(path) {
212 if (!path.node.tag.arguments.length) {
213 throw path
214 .get('tag')
215 .buildCodeFrameError(
216 'match() must at least be called with a node name'
217 );
218 }
219
220 const name = path.node.tag.arguments[0];
221 const quasis = path.node.quasi.quasis.map((x) => x.value.cooked);
222
223 const expressions = this._prepareExpressions(path);
224 const transform = this._prepareTransform(path);
225
226 let ast;
227 try {
228 ast = parse(quasis, expressions);
229 } catch (error) {
230 if (error.name !== 'SyntaxError') throw error;
231 throw path.get('quasi').buildCodeFrameError(error.message);
232 }
233
234 const code = astRoot(ast, '%%name%%', transform && '%%transform%%');
235
236 path.replaceWith(
237 template.expression(code)(transform ? { name, transform } : { name })
238 );
239 },
240 };
241}