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}