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 if (nameArgumentPath) {
87 const { confident, value } = nameArgumentPath.evaluate();
88 if (!confident && t.isIdentifier(nameArgumentPath.node)) {
89 return nameArgumentPath.node.name;
90 } else if (confident && typeof value === 'string') {
91 return value;
92 }
93 }
94
95 return path.scope.generateUidIdentifierBasedOnNode(path.node);
96 },
97
98 /** Given a match, hoists its expressions in front of the match's statement */
99 _prepareExpressions(path) {
100 t.assertTaggedTemplateExpression(path.node);
101
102 const variableDeclarators = [];
103 const matchName = this.getMatchName(path);
104
105 const hoistedExpressions = path.node.quasi.expressions.map(
106 (expression, i) => {
107 if (
108 t.isArrowFunctionExpression(expression) &&
109 t.isIdentifier(expression.body)
110 ) {
111 expression = expression.body;
112 } else if (
113 (t.isFunctionExpression(expression) ||
114 t.isArrowFunctionExpression(expression)) &&
115 t.isBlockStatement(expression.body) &&
116 expression.body.body.length === 1 &&
117 t.isReturnStatement(expression.body.body[0]) &&
118 t.isIdentifier(expression.body.body[0].argument)
119 ) {
120 expression = expression.body.body[0].argument;
121 }
122
123 const isBindingExpression =
124 t.isIdentifier(expression) &&
125 path.scope.hasBinding(expression.name);
126 if (isBindingExpression) {
127 const binding = path.scope.getBinding(expression.name);
128 if (t.isVariableDeclarator(binding.path.node)) {
129 const matchPath = binding.path.get('init');
130 if (this.isMatch(matchPath)) {
131 return expression;
132 } else if (_hoistedExpressions.has(expression.name)) {
133 return t.identifier(_hoistedExpressions.get(expression.name));
134 }
135 }
136 }
137
138 const id = path.scope.generateUidIdentifier(
139 isBindingExpression
140 ? `${expression.name}_expression`
141 : `${matchName}_expression`
142 );
143
144 variableDeclarators.push(
145 t.variableDeclarator(
146 id,
147 t.callExpression(t.identifier(_patternId.name), [expression])
148 )
149 );
150
151 if (t.isIdentifier(expression)) {
152 _hoistedExpressions.set(expression.name, id.name);
153 }
154
155 return id;
156 }
157 );
158
159 if (variableDeclarators.length) {
160 path
161 .getStatementParent()
162 .insertBefore(t.variableDeclaration('var', variableDeclarators));
163 }
164
165 return hoistedExpressions.map((id) => {
166 const binding = path.scope.getBinding(id.name);
167 if (binding && t.isVariableDeclarator(binding.path.node)) {
168 const matchPath = binding.path.get('init');
169 if (this.isMatch(matchPath)) {
170 return { fn: true, id: id.name };
171 }
172 }
173
174 const input = t.isStringLiteral(id)
175 ? JSON.stringify(id.value)
176 : id.name;
177 return { fn: false, id: input };
178 });
179 },
180
181 _prepareTransform(path) {
182 const transformNode = path.node.tag.arguments[1];
183
184 if (!transformNode) return null;
185 if (t.isIdentifier(transformNode)) return transformNode.name;
186
187 const matchName = this.getMatchName(path);
188 const id = path.scope.generateUidIdentifier(`${matchName}_transform`);
189 const declarator = t.variableDeclarator(id, transformNode);
190
191 path
192 .getStatementParent()
193 .insertBefore(t.variableDeclaration('var', [declarator]));
194
195 return id.name;
196 },
197
198 minifyMatch(path) {
199 const quasis = path.node.quasi.quasis.map((x) =>
200 t.stringLiteral(x.value.cooked.replace(/\s*/g, ''))
201 );
202 const expressions = path.node.quasi.expressions;
203 const transform = this._prepareTransform(path);
204
205 path.replaceWith(
206 t.callExpression(path.node.tag, [
207 t.arrayExpression(quasis),
208 ...expressions,
209 ])
210 );
211 },
212
213 transformMatch(path) {
214 let name = path.node.tag.arguments[0];
215 if (!name) {
216 name = t.nullLiteral();
217 }
218
219 const quasis = path.node.quasi.quasis.map((x) => x.value.cooked);
220
221 const expressions = this._prepareExpressions(path);
222 const transform = this._prepareTransform(path);
223
224 let ast;
225 try {
226 ast = parse(quasis, expressions);
227 } catch (error) {
228 if (error.name !== 'SyntaxError') throw error;
229 throw path.get('quasi').buildCodeFrameError(error.message);
230 }
231
232 const code = astRoot(ast, '%%name%%', transform && '%%transform%%');
233
234 path.replaceWith(
235 template.expression(code)(transform ? { name, transform } : { name })
236 );
237 },
238 };
239}