-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
ClosureModuleProcessor.java
387 lines (350 loc) · 14 KB
/
ClosureModuleProcessor.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
/*
* Copyright 2019 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp.modules;
import static com.google.javascript.jscomp.modules.ModuleMapCreator.DOES_NOT_HAVE_EXPORT;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.javascript.jscomp.AbstractCompiler;
import com.google.javascript.jscomp.JSError;
import com.google.javascript.jscomp.NodeTraversal;
import com.google.javascript.jscomp.NodeTraversal.AbstractPreOrderCallback;
import com.google.javascript.jscomp.deps.ModuleLoader.ModulePath;
import com.google.javascript.jscomp.modules.ClosureRequireProcessor.Require;
import com.google.javascript.jscomp.modules.ModuleMapCreator.ModuleProcessor;
import com.google.javascript.jscomp.modules.ModuleMetadataMap.ModuleMetadata;
import com.google.javascript.rhino.Node;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Processor for goog.module
*
* <p>The namespace of a goog.module contains all named exports, e.g. {@code exports.x = 0}, and any
* 'default export's that assign directly to the `exports` object, e.g. {@code exports = class {}}).
*
* <p>The bound names include any names imported through a goog.require(Type)/forwardDeclare.
*/
final class ClosureModuleProcessor implements ModuleProcessor {
private static class UnresolvedGoogModule extends UnresolvedModule {
private final ModuleMetadata metadata;
private final String srcFileName;
@Nullable private final ModulePath path;
private final ImmutableMap<String, Binding> namespace;
private final ImmutableMap<String, Require> requiresByLocalName;
private final AbstractCompiler compiler;
private Module resolved = null;
UnresolvedGoogModule(
ModuleMetadata metadata,
String srcFileName,
ModulePath path,
ImmutableMap<String, Binding> namespace,
ImmutableMap<String, Require> requires,
AbstractCompiler compiler) {
this.metadata = metadata;
this.srcFileName = srcFileName;
this.path = path;
this.namespace = namespace;
this.requiresByLocalName = requires;
this.compiler = compiler;
}
@Nullable
@Override
public ResolveExportResult resolveExport(
ModuleRequestResolver moduleRequestResolver, String exportName) {
if (namespace.containsKey(exportName)) {
return ResolveExportResult.of(namespace.get(exportName));
}
return ResolveExportResult.NOT_FOUND;
}
@Nullable
@Override
public ResolveExportResult resolveExport(
ModuleRequestResolver moduleRequestResolver,
@Nullable String moduleSpecifier,
String exportName,
Set<ExportTrace> resolveSet,
Set<UnresolvedModule> exportStarSet) {
return resolveExport(moduleRequestResolver, exportName);
}
@Override
public Module resolve(
ModuleRequestResolver moduleRequestResolver, @Nullable String moduleSpecifier) {
if (resolved == null) {
// Every import creates a locally bound name.
Map<String, Binding> boundNames =
new LinkedHashMap<>(getAllResolvedImports(moduleRequestResolver));
resolved =
Module.builder()
.path(path)
.metadata(metadata)
.namespace(namespace)
.boundNames(ImmutableMap.copyOf(boundNames))
.localNameToLocalExport(ImmutableMap.of())
.closureNamespace(Iterables.getOnlyElement(metadata.googNamespaces()))
.unresolvedModule(this)
.build();
}
return resolved;
}
/** A map from import bound name to binding. */
Map<String, Binding> getAllResolvedImports(ModuleRequestResolver moduleRequestResolver) {
Map<String, Binding> imports = new HashMap<>();
for (String name : requiresByLocalName.keySet()) {
ResolveExportResult b = resolveImport(moduleRequestResolver, name);
if (b.resolved()) {
imports.put(name, b.getBinding());
}
}
return imports;
}
ResolveExportResult resolveImport(ModuleRequestResolver moduleRequestResolver, String name) {
Require require = requiresByLocalName.get(name);
Import importRecord = require.importRecord();
UnresolvedModule requested = moduleRequestResolver.resolve(importRecord);
if (requested == null) {
return ResolveExportResult.ERROR;
} else if (importRecord.importName().equals(Export.NAMESPACE)) {
// Return a binding based on the other module's metadata.
return ResolveExportResult.of(
Binding.from(
requested.metadata(),
requested.metadata().rootNode(),
importRecord.moduleRequest(),
require.createdBy()));
} else {
ResolveExportResult result =
requested.resolveExport(
moduleRequestResolver,
importRecord.moduleRequest(),
importRecord.importName(),
new HashSet<>(),
new HashSet<>());
if (!result.found() && !result.hadError()) {
compiler.report(
JSError.make(
srcFileName,
importRecord.importNode().getLineno(),
importRecord.importNode().getCharno(),
DOES_NOT_HAVE_EXPORT,
importRecord.importName()));
return ResolveExportResult.ERROR;
}
Node forSourceInfo =
importRecord.nameNode() == null ? importRecord.importNode() : importRecord.nameNode();
return result.copy(forSourceInfo, require.createdBy());
}
}
@Override
ModuleMetadata metadata() {
return metadata;
}
@Override
public ImmutableSet<String> getExportedNames(ModuleRequestResolver moduleRequestResolver) {
// Unsupported until such time as it becomes useful
throw new UnsupportedOperationException();
}
@Override
public ImmutableSet<String> getExportedNames(
ModuleRequestResolver moduleRequestResolver, Set<UnresolvedModule> visited) {
throw new UnsupportedOperationException();
}
@Override
void reset() {
resolved = null;
}
}
private final AbstractCompiler compiler;
public ClosureModuleProcessor(AbstractCompiler compiler) {
this.compiler = compiler;
}
@Override
public UnresolvedModule process(ModuleMetadata metadata, ModulePath path, Node script) {
Preconditions.checkArgument(
script.isScript() || script.isCall(), "Unexpected module root %s", script);
Preconditions.checkArgument(
script.isCall() || path != null, "Non goog.loadModules must have a path");
ModuleProcessingCallback moduleProcessingCallback = new ModuleProcessingCallback(metadata);
NodeTraversal.traverse(compiler, script, moduleProcessingCallback);
return new UnresolvedGoogModule(
metadata,
script.getSourceFileName(),
path,
ImmutableMap.copyOf(moduleProcessingCallback.namespace),
ImmutableMap.copyOf(moduleProcessingCallback.requiresByLocalName),
compiler);
}
/** Traverses a subtree rooted at a module, gathering all exports and requires */
private static class ModuleProcessingCallback extends AbstractPreOrderCallback {
private final ModuleMetadata metadata;
/** The Closure namespace 'a.b.c' from the `goog.module('a.b.c');` statement */
private final String closureNamespace;
// Note: the following two maps are mutable because in some cases, we need to check if a key has
// already been added before trying to add a second.
/** All named exports and explicit assignments of the `exports` object */
private final Map<String, Binding> namespace;
/** All required/forwardDeclared local names */
private final Map<String, Require> requiresByLocalName;
/** Whether we've come across an "exports = ..." assignment */
private boolean seenExportsAssignment;
ModuleProcessingCallback(ModuleMetadata metadata) {
this.metadata = metadata;
this.namespace = new LinkedHashMap<>();
this.requiresByLocalName = new LinkedHashMap<>();
this.closureNamespace = Iterables.getOnlyElement(metadata.googNamespaces());
this.seenExportsAssignment = false;
}
@Override
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
switch (n.getToken()) {
case MODULE_BODY:
case SCRIPT:
case CALL: // Traverse into goog.loadModule calls.
case BLOCK:
return true;
case FUNCTION:
// Only traverse into functions that are the argument of a goog.loadModule call, which is
// the module root. Avoid traversing function declarations like:
// goog.module('a.b'); function (exports) { exports.x = 0; }
return parent.isCall() && parent == metadata.rootNode();
case EXPR_RESULT:
Node expr = n.getFirstChild();
if (expr.isAssign()) {
maybeInitializeExports(expr);
} else if (expr.isGetProp()) {
maybeInitializeExportsStub(expr);
}
return false;
case CONST:
case VAR:
case LET:
// Note that `let` is valid only for `goog.forwardDeclare`.
maybeInitializeRequire(n);
return false;
default:
return false;
}
}
/** If an assignment is to 'exports', adds it to the list of Exports */
private void maybeInitializeExports(Node assignment) {
Node lhs = assignment.getFirstChild();
Node rhs = assignment.getSecondChild();
if (lhs.isName() && lhs.getString().equals("exports")) {
// This may be a 'named exports' or may be a default export.
// It is a 'named export' if and only if it is assigned an object literal w/ string keys,
// whose values are all names.
if (isNamedExportsLiteral(rhs)) {
initializeNamedExportsLiteral(rhs);
} else {
seenExportsAssignment = true;
}
markExportsAssignmentInNamespace(lhs);
} else if (lhs.isGetProp()
&& lhs.getFirstChild().isName()
&& lhs.getFirstChild().getString().equals("exports")) {
String exportedId = lhs.getSecondChild().getString();
addPropertyExport(exportedId, lhs);
}
}
/** Adds stub export declarations `exports.Foo;` to the list of Exports */
private void maybeInitializeExportsStub(Node qname) {
Node owner = qname.getFirstChild();
if (owner.isName() && owner.getString().equals("exports")) {
Node prop = qname.getSecondChild();
String exportedId = prop.getString();
addPropertyExport(exportedId, qname);
}
}
/**
* Adds an explicit namespace export.
*
* <p>Note that all goog.modules create an 'exports' object, but this object is only added to
* the Module namespace if there is an explicit' exports = ...' assignment
*/
private void markExportsAssignmentInNamespace(Node exportsNode) {
namespace.put(
Export.NAMESPACE,
Binding.from(
Export.builder()
.exportName(Export.NAMESPACE)
.exportNode(exportsNode)
.moduleMetadata(metadata)
.closureNamespace(closureNamespace)
.modulePath(metadata.path())
.build(),
exportsNode));
}
private void initializeNamedExportsLiteral(Node objectLit) {
for (Node key : objectLit.children()) {
addPropertyExport(key.getString(), key);
}
}
/** Adds a named export to the list of Exports */
private void addPropertyExport(String exportedId, Node propNode) {
if (seenExportsAssignment) {
// We've seen an assignment "exports = ...", so this is not a named export.
return;
} else if (namespace.containsKey(exportedId)) {
// Ignore duplicate exports - this is an error but checked elsewhere.
return;
}
namespace.put(
exportedId,
Binding.from(
Export.builder()
.exportName(exportedId)
.exportNode(propNode)
.moduleMetadata(metadata)
.closureNamespace(closureNamespace)
.modulePath(metadata.path())
.build(),
propNode));
}
/** Adds a goog.require(Type) or forwardDeclare to the list of {@code requiresByLocalName} */
private void maybeInitializeRequire(Node nameDeclaration) {
for (Require require : ClosureRequireProcessor.getAllRequires(nameDeclaration)) {
requiresByLocalName.putIfAbsent(require.localName(), require);
}
}
}
/**
* Whether this is an assignment to 'exports' that creates named exports.
*
* <ul>
* <li>exports = {a, b}; // named exports
* <li>exports = 0; // namespace export
* <li>exports = {a: 0, b}; // namespace export
* </ul>
*/
private static boolean isNamedExportsLiteral(Node objLit) {
if (!objLit.isObjectLit() || !objLit.hasChildren()) {
return false;
}
for (Node key : objLit.children()) {
if (!key.isStringKey() || key.isQuotedString()) {
return false;
}
if (key.hasChildren() && !key.getFirstChild().isName()) {
return false;
}
}
return true;
}
}