1import { parse } from '../parser';
2import { SharedIds } from './sharedIds';
3import { initGenerator, RootNode } from './generator';
4
5export function makeHelpers(t) {
6 const importSourceRe = /reghex$|^reghex\/macro/;
7 const importName = 'reghex';
8 const ids = new SharedIds(t);
9 initGenerator(ids, 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 const defaultSpecifierIndex = path.node.specifiers.findIndex((node) => {
21 return t.isImportDefaultSpecifier(node);
22 });
23
24 if (defaultSpecifierIndex > -1) {
25 path.node.specifiers.splice(defaultSpecifierIndex, 1);
26 }
27
28 if (path.node.source.value !== importName) {
29 path.node.source = t.stringLiteral(importName);
30 }
31
32 path.node.specifiers.push(
33 t.importSpecifier(
34 (ids.execId = path.scope.generateUidIdentifier('exec')),
35 t.identifier('_exec')
36 ),
37 t.importSpecifier(
38 (ids.patternId = path.scope.generateUidIdentifier('pattern')),
39 t.identifier('_pattern')
40 )
41 );
42
43 const tagImport = path.node.specifiers.find((node) => {
44 return t.isImportSpecifier(node) && node.imported.name === 'tag';
45 });
46 if (!tagImport) {
47 path.node.specifiers.push(
48 t.importSpecifier(
49 (ids.tagId = path.scope.generateUidIdentifier('tag')),
50 t.identifier('tag')
51 )
52 );
53 } else {
54 ids.tagId = tagImport.imported;
55 }
56 },
57
58 /** Determines whether the given tagged template expression is a reghex match */
59 isMatch(path) {
60 if (
61 t.isTaggedTemplateExpression(path.node) &&
62 t.isCallExpression(path.node.tag) &&
63 t.isIdentifier(path.node.tag.callee) &&
64 path.scope.hasBinding(path.node.tag.callee.name)
65 ) {
66 if (t.isVariableDeclarator(path.parentPath))
67 path.parentPath._isMatch = true;
68 return true;
69 }
70
71 return (
72 t.isVariableDeclarator(path.parentPath) && path.parentPath._isMatch
73 );
74 },
75
76 /** Given a reghex match, returns the path to reghex's match import declaration */
77 getMatchImport(path) {
78 t.assertTaggedTemplateExpression(path.node);
79 const binding = path.scope.getBinding(path.node.tag.callee.name);
80
81 if (
82 binding.kind !== 'module' ||
83 !t.isImportDeclaration(binding.path.parent) ||
84 !importSourceRe.test(binding.path.parent.source.value) ||
85 !t.isImportDefaultSpecifier(binding.path.node)
86 ) {
87 return null;
88 }
89
90 return binding.path.parentPath;
91 },
92
93 /** Given a match, returns an evaluated name or a best guess */
94 getMatchName(path) {
95 t.assertTaggedTemplateExpression(path.node);
96 const nameArgumentPath = path.get('tag.arguments.0');
97 const { confident, value } = nameArgumentPath.evaluate();
98 if (!confident && t.isIdentifier(nameArgumentPath.node)) {
99 return nameArgumentPath.node.name;
100 } else if (confident && typeof value === 'string') {
101 return value;
102 } else {
103 return path.scope.generateUidIdentifierBasedOnNode(path.node);
104 }
105 },
106
107 /** Given a match, hoists its expressions in front of the match's statement */
108 _hoistExpressions(path) {
109 t.assertTaggedTemplateExpression(path.node);
110
111 const variableDeclarators = [];
112 const matchName = this.getMatchName(path);
113
114 const hoistedExpressions = path.node.quasi.expressions.map(
115 (expression) => {
116 if (
117 t.isIdentifier(expression) &&
118 path.scope.hasBinding(expression.name)
119 ) {
120 const binding = path.scope.getBinding(expression.name);
121 if (t.isVariableDeclarator(binding.path.node)) {
122 const matchPath = binding.path.get('init');
123 if (this.isMatch(matchPath)) return expression;
124 }
125 }
126
127 const id = path.scope.generateUidIdentifier(
128 `${matchName}_expression`
129 );
130 variableDeclarators.push(
131 t.variableDeclarator(
132 id,
133 t.callExpression(ids.pattern, [expression])
134 )
135 );
136 return id;
137 }
138 );
139
140 if (variableDeclarators.length) {
141 path
142 .getStatementParent()
143 .insertBefore(t.variableDeclaration('var', variableDeclarators));
144 }
145
146 return hoistedExpressions;
147 },
148
149 _hoistTransform(path) {
150 const transformNode = path.node.tag.arguments[1];
151 if (!transformNode) return null;
152 if (t.isIdentifier(transformNode)) return transformNode;
153
154 const matchName = this.getMatchName(path);
155 const id = path.scope.generateUidIdentifier(`${matchName}_transform`);
156 const declarator = t.variableDeclarator(id, transformNode);
157
158 path
159 .getStatementParent()
160 .insertBefore(t.variableDeclaration('var', [declarator]));
161 return id;
162 },
163
164 transformMatch(path) {
165 if (!path.node.tag.arguments.length) {
166 throw path
167 .get('tag')
168 .buildCodeFrameError(
169 'match() must at least be called with a node name'
170 );
171 }
172
173 const matchName = this.getMatchName(path);
174 const nameNode = path.node.tag.arguments[0];
175 const quasis = path.node.quasi.quasis.map((x) => x.value.cooked);
176
177 // Hoist expressions and wrap them in an execPattern call
178 const expressions = this._hoistExpressions(path).map((id) => {
179 // Directly call expression if it's sure to be another matcher
180 const binding = path.scope.getBinding(id.name);
181 if (binding && t.isVariableDeclarator(binding.path.node)) {
182 const matchPath = binding.path.get('init');
183 if (this.isMatch(matchPath)) {
184 return t.callExpression(id, [ids.state]);
185 }
186 }
187
188 return t.callExpression(ids.exec, [ids.state, id]);
189 });
190
191 // Hoist transform argument if necessary
192 const transformNode = this._hoistTransform(path);
193
194 let ast;
195 try {
196 ast = parse(quasis, expressions);
197 } catch (error) {
198 if (error.name !== 'SyntaxError') throw error;
199 throw path.get('quasi').buildCodeFrameError(error.message);
200 }
201
202 const generator = new RootNode(ast, nameNode, transformNode);
203 const body = t.blockStatement(generator.statements());
204 const matchFunctionId = path.scope.generateUidIdentifier(matchName);
205 const matchFunction = t.functionExpression(
206 matchFunctionId,
207 [ids.state],
208 body
209 );
210 path.replaceWith(matchFunction);
211 },
212 };
213}