7
8
import {NodePath} from '@babel/core';
9
import * as t from '@babel/types';
10
+import {Scope as BabelScope} from '@babel/traverse';
11
+
12
import {CompilerError, ErrorSeverity} from '../CompilerError';
11
-import {EnvironmentConfig, ExternalFunction, GeneratedSource} from '../HIR';
12
-import {getOrInsertDefault} from '../Utils/utils';
13
+import {
14
+ EnvironmentConfig,
15
+ GeneratedSource,
16
+ NonLocalImportSpecifier,
17
+} from '../HIR';
18
+import {getOrInsertWith} from '../Utils/utils';
19
+import {ExternalFunction, isHookName} from '../HIR/Environment';
20
+import {Err, Ok, Result} from '../Utils/Result';
21
+import {CompilerReactTarget} from './Options';
22
+import {getReactCompilerRuntimeModule} from './Program';
23
24
export function validateRestrictedImports(
25
path: NodePath<t.Program>,
52
}
53
}
54
45
-export function addImportsToProgram(
46
- path: NodePath<t.Program>,
47
- importList: Array<ExternalFunction>,
48
-): void {
49
- const identifiers: Set<string> = new Set();
50
- const sortedImports: Map<string, Array<string>> = new Map();
51
- for (const {importSpecifierName, source} of importList) {
52
- /*
53
- * Codegen currently does not rename import specifiers, so we do additional
54
- * validation here
55
+export class ProgramContext {
56
+ /* Program and environment context */
57
+ scope: BabelScope;
58
+ reactRuntimeModule: string;
59
+ hookPattern: string | null;
60
+
61
+ // known generated or referenced identifiers in the program
62
+ knownReferencedNames: Set<string> = new Set();
63
+ // generated imports
64
+ imports: Map<string, Map<string, NonLocalImportSpecifier>> = new Map();
65
+
66
+ constructor(
67
+ program: NodePath<t.Program>,
68
+ reactRuntimeModule: CompilerReactTarget,
69
+ hookPattern: string | null,
70
+ ) {
71
+ this.hookPattern = hookPattern;
72
+ this.scope = program.scope;
73
+ this.reactRuntimeModule = getReactCompilerRuntimeModule(reactRuntimeModule);
74
+ }
75
+
76
+ isHookName(name: string): boolean {
77
+ if (this.hookPattern == null) {
78
+ return isHookName(name);
79
+ } else {
80
+ const match = new RegExp(this.hookPattern).exec(name);
81
+ return (
82
+ match != null && typeof match[1] === 'string' && isHookName(match[1])
83
+ );
84
+ }
85
+ }
86
+
87
+ hasReference(name: string): boolean {
88
+ return (
89
+ this.knownReferencedNames.has(name) ||
90
+ this.scope.hasBinding(name) ||
91
+ this.scope.hasGlobal(name) ||
92
+ this.scope.hasReference(name)
93
+ );
94
+ }
95
+
96
+ newUid(name: string): string {
97
+ /**
98
+ * Don't call babel's generateUid for known hook imports, as
99
+ * InferTypes might eventually type `HookKind` based on callee naming
100
+ * convention and `_useFoo` is not named as a hook.
101
+ *
102
+ * Local uid generation is susceptible to check-before-use bugs since we're
103
+ * checking for naming conflicts / references long before we actually insert
104
+ * the import. (see similar logic in HIRBuilder:resolveBinding)
105
*/
56
- CompilerError.invariant(identifiers.has(importSpecifierName) === false, {
57
- reason: `Encountered conflicting import specifier for ${importSpecifierName} in Forget config.`,
58
- description: null,
59
- loc: GeneratedSource,
60
- suggestions: null,
61
- });
62
- CompilerError.invariant(
63
- path.scope.hasBinding(importSpecifierName) === false,
106
+ let uid;
107
+ if (this.isHookName(name)) {
108
+ uid = name;
109
+ let i = 0;
110
+ while (this.hasReference(uid)) {
111
+ this.knownReferencedNames.add(uid);
112
+ uid = `${name}_${i++}`;
113
+ }
114
+ } else if (!this.hasReference(name)) {
115
+ uid = name;
116
+ } else {
117
+ uid = this.scope.generateUid(name);
118
+ }
119
+ this.knownReferencedNames.add(uid);
120
+ return uid;
121
+ }
122
+
123
+ addMemoCacheImport(): NonLocalImportSpecifier {
124
+ return this.addImportSpecifier(
125
{
65
- reason: `Encountered conflicting import specifiers for ${importSpecifierName} in generated program.`,
66
- description: null,
67
- loc: GeneratedSource,
68
- suggestions: null,
126
+ source: this.reactRuntimeModule,
127
+ importSpecifierName: 'c',
128
},
129
+ '_c',
130
);
71
- identifiers.add(importSpecifierName);
72
-
73
- const importSpecifierNameList = getOrInsertDefault(
74
- sortedImports,
75
- source,
76
- [],
77
- );
78
- importSpecifierNameList.push(importSpecifierName);
131
}
132
81
- const stmts: Array<t.ImportDeclaration> = [];
82
- for (const [source, importSpecifierNameList] of sortedImports) {
83
- const importSpecifiers = importSpecifierNameList.map(name => {
84
- const id = t.identifier(name);
85
- return t.importSpecifier(id, id);
86
- });
133
+ /**
134
+ *
135
+ * @param externalFunction
136
+ * @param nameHint if defined, will be used as the name of the import specifier
137
+ * @returns
138
+ */
139
+ addImportSpecifier(
140
+ {source: module, importSpecifierName: specifier}: ExternalFunction,
141
+ nameHint?: string,
142
+ ): NonLocalImportSpecifier {
143
+ const maybeBinding = this.imports.get(module)?.get(specifier);
144
+ if (maybeBinding != null) {
145
+ return {...maybeBinding};
146
+ }
147
88
- stmts.push(t.importDeclaration(importSpecifiers, t.stringLiteral(source)));
148
+ const binding: NonLocalImportSpecifier = {
149
+ kind: 'ImportSpecifier',
150
+ name: this.newUid(nameHint ?? specifier),
151
+ module,
152
+ imported: specifier,
153
+ };
154
+ getOrInsertWith(this.imports, module, () => new Map()).set(specifier, {
155
+ ...binding,
156
+ });
157
+ return binding;
158
}
90
- path.unshiftContainer('body', stmts);
91
-}
92
-
93
-/*
94
- * Matches `import { ... } from <moduleName>;`
95
- * but not `import * as React from <moduleName>;`
96
- */
97
-function isNonNamespacedImport(
98
- importDeclPath: NodePath<t.ImportDeclaration>,
99
- moduleName: string,
100
-): boolean {
101
- return (
102
- importDeclPath.get('source').node.value === moduleName &&
103
- importDeclPath
104
- .get('specifiers')
105
- .every(specifier => specifier.isImportSpecifier()) &&
106
- importDeclPath.node.importKind !== 'type' &&
107
- importDeclPath.node.importKind !== 'typeof'
108
- );
109
-}
159
111
-function hasExistingNonNamespacedImportOfModule(
112
- program: NodePath<t.Program>,
113
- moduleName: string,
114
-): boolean {
115
- let hasExistingImport = false;
116
- program.traverse({
117
- ImportDeclaration(importDeclPath) {
118
- if (isNonNamespacedImport(importDeclPath, moduleName)) {
119
- hasExistingImport = true;
120
- }
121
- },
122
- });
160
+ addNewReference(name: string): void {
161
+ this.knownReferencedNames.add(name);
162
+ }
163
124
- return hasExistingImport;
164
+ assertGlobalBinding(
165
+ name: string,
166
+ localScope?: BabelScope,
167
+ ): Result<void, CompilerError> {
168
+ const scope = localScope ?? this.scope;
169
+ if (!scope.hasReference(name) && !scope.hasBinding(name)) {
170
+ return Ok(undefined);
171
+ }
172
+ const error = new CompilerError();
173
+ error.push({
174
+ severity: ErrorSeverity.Todo,
175
+ reason: 'Encountered conflicting global in generated program',
176
+ description: `Conflict from local binding ${name}`,
177
+ loc: scope.getBinding(name)?.path.node.loc ?? null,
178
+ suggestions: null,
179
+ });
180
+ return Err(error);
181
+ }
182
}
183
127
-/*
128
- * If an existing import of React exists (ie `import { ... } from '<moduleName>'`), inject useMemoCache
129
- * into the list of destructured variables.
130
- */
131
-function addMemoCacheFunctionSpecifierToExistingImport(
184
+function getExistingImports(
185
program: NodePath<t.Program>,
133
- moduleName: string,
134
- identifierName: string,
135
-): boolean {
136
- let didInsertUseMemoCache = false;
186
+): Map<string, NodePath<t.ImportDeclaration>> {
187
+ const existingImports = new Map<string, NodePath<t.ImportDeclaration>>();
188
program.traverse({
138
- ImportDeclaration(importDeclPath) {
139
- if (
140
- !didInsertUseMemoCache &&
141
- isNonNamespacedImport(importDeclPath, moduleName)
142
- ) {
143
- importDeclPath.pushContainer(
144
- 'specifiers',
145
- t.importSpecifier(t.identifier(identifierName), t.identifier('c')),
146
- );
147
- didInsertUseMemoCache = true;
189
+ ImportDeclaration(path) {
190
+ if (isNonNamespacedImport(path)) {
191
+ existingImports.set(path.node.source.value, path);
192
}
193
},
194
});
151
- return didInsertUseMemoCache;
195
+ return existingImports;
196
}
197
154
-export function updateMemoCacheFunctionImport(
155
- program: NodePath<t.Program>,
156
- moduleName: string,
157
- useMemoCacheIdentifier: string,
198
+export function addImportsToProgram(
199
+ path: NodePath<t.Program>,
200
+ programContext: ProgramContext,
201
): void {
159
- /*
160
- * If there isn't already an import of * as React, insert it so useMemoCache doesn't
161
- * throw
162
- */
163
- const hasExistingImport = hasExistingNonNamespacedImportOfModule(
164
- program,
165
- moduleName,
202
+ const existingImports = getExistingImports(path);
203
+ const stmts: Array<t.ImportDeclaration> = [];
204
+ const sortedModules = [...programContext.imports.entries()].sort(([a], [b]) =>
205
+ a.localeCompare(b),
206
);
207
+ for (const [moduleName, importsMap] of sortedModules) {
208
+ for (const [specifierName, loweredImport] of importsMap) {
209
+ /**
210
+ * Assert that the import identifier hasn't already be declared in the program.
211
+ * Note: we use getBinding here since `Scope.hasBinding` pessimistically returns true
212
+ * for all allocated uids (from `Scope.getUid`)
213
+ */
214
+ CompilerError.invariant(
215
+ path.scope.getBinding(loweredImport.name) == null,
216
+ {
217
+ reason:
218
+ 'Encountered conflicting import specifiers in generated program',
219
+ description: `Conflict from import ${loweredImport.module}:(${loweredImport.imported} as ${loweredImport.name}).`,
220
+ loc: GeneratedSource,
221
+ suggestions: null,
222
+ },
223
+ );
224
+ CompilerError.invariant(
225
+ loweredImport.module === moduleName &&
226
+ loweredImport.imported === specifierName,
227
+ {
228
+ reason:
229
+ 'Found inconsistent import specifier. This is an internal bug.',
230
+ description: `Expected import ${moduleName}:${specifierName} but found ${loweredImport.module}:${loweredImport.imported}`,
231
+ loc: GeneratedSource,
232
+ },
233
+ );
234
+ }
235
+ const sortedImport: Array<NonLocalImportSpecifier> = [
236
+ ...importsMap.values(),
237
+ ].sort(({imported: a}, {imported: b}) => a.localeCompare(b));
238
+ const importSpecifiers = sortedImport.map(specifier => {
239
+ return t.importSpecifier(
240
+ t.identifier(specifier.name),
241
+ t.identifier(specifier.imported),
242
+ );
243
+ });
244
168
- if (hasExistingImport) {
169
- const didUpdateImport = addMemoCacheFunctionSpecifierToExistingImport(
170
- program,
171
- moduleName,
172
- useMemoCacheIdentifier,
173
- );
174
- if (!didUpdateImport) {
175
- throw new Error(
176
- `Expected an ImportDeclaration of \`${moduleName}\` in order to update ImportSpecifiers with useMemoCache`,
245
+ /**
246
+ * If an existing import of this module exists (ie `import { ... } from
247
+ * '<moduleName>'`), inject new imported specifiers into the list of
248
+ * destructured variables.
249
+ */
250
+ const maybeExistingImports = existingImports.get(moduleName);
251
+ if (maybeExistingImports != null) {
252
+ maybeExistingImports.pushContainer('specifiers', importSpecifiers);
253
+ } else {
254
+ stmts.push(
255
+ t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
256
);
257
}
179
- } else {
180
- addMemoCacheFunctionImportDeclaration(
181
- program,
182
- moduleName,
183
- useMemoCacheIdentifier,
184
- );
258
}
259
+ path.unshiftContainer('body', stmts);
260
}
261
188
-function addMemoCacheFunctionImportDeclaration(
189
- program: NodePath<t.Program>,
190
- moduleName: string,
191
- localName: string,
192
-): void {
193
- program.unshiftContainer(
194
- 'body',
195
- t.importDeclaration(
196
- [t.importSpecifier(t.identifier(localName), t.identifier('c'))],
197
- t.stringLiteral(moduleName),
198
- ),
262
+/*
263
+ * Matches `import { ... } from <moduleName>;`
264
+ * but not `import * as React from <moduleName>;`
265
+ * `import type { Foo } from <moduleName>;`
266
+ */
267
+function isNonNamespacedImport(
268
+ importDeclPath: NodePath<t.ImportDeclaration>,
269
+): boolean {
270
+ return (
271
+ importDeclPath
272
+ .get('specifiers')
273
+ .every(specifier => specifier.isImportSpecifier()) &&
274
+ importDeclPath.node.importKind !== 'type' &&
275
+ importDeclPath.node.importKind !== 'typeof'
276
);
277
}