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