Skip to content

Commit db80ab2

Browse files
shicksDimitris Vardoulakis
authored andcommitted
Teach deps.JsFileParser to understand ES6 modules.
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=129388646
1 parent fae1b9b commit db80ab2

File tree

7 files changed

+249
-46
lines changed

7 files changed

+249
-46
lines changed

src/com/google/javascript/jscomp/LazyParsedDependencyInfo.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
import com.google.common.base.Preconditions;
2020
import com.google.common.collect.ImmutableMap;
2121
import com.google.javascript.jscomp.deps.DependencyInfo;
22+
import com.google.javascript.jscomp.deps.ModuleLoader;
2223
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
23-
2424
import java.util.Collection;
2525
import java.util.Map;
2626
import java.util.TreeMap;
@@ -30,9 +30,6 @@
3030
*/
3131
public class LazyParsedDependencyInfo implements DependencyInfo {
3232

33-
static final DiagnosticType MODULE_CONFLICT = DiagnosticType.warning(
34-
"JSC_MODULE_CONFLICT", "File has both goog.module and ES6 modules: {0}");
35-
3633
private final DependencyInfo delegate;
3734
private final JsAst ast;
3835
private final AbstractCompiler compiler;
@@ -52,8 +49,9 @@ public ImmutableMap<String, String> getLoadFlags() {
5249
loadFlagsBuilder.putAll(delegate.getLoadFlags());
5350
FeatureSet features = ((JsAst) ast).getFeatures(compiler);
5451
if (features.hasEs6Modules()) {
55-
if (loadFlagsBuilder.containsKey("module")) {
56-
compiler.report(JSError.make(MODULE_CONFLICT, getName()));
52+
String previousModule = loadFlagsBuilder.get("module");
53+
if (previousModule != null && !previousModule.equals("es6")) {
54+
compiler.report(JSError.make(ModuleLoader.MODULE_CONFLICT, getName()));
5755
}
5856
loadFlagsBuilder.put("module", "es6");
5957
}

src/com/google/javascript/jscomp/deps/DepsGenerator.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import com.google.javascript.jscomp.JsAst;
3232
import com.google.javascript.jscomp.LazyParsedDependencyInfo;
3333
import com.google.javascript.jscomp.SourceFile;
34-
3534
import java.io.ByteArrayOutputStream;
3635
import java.io.File;
3736
import java.io.IOException;
@@ -68,6 +67,7 @@ public static enum InclusionStrategy {
6867
private final Collection<SourceFile> deps;
6968
private final String closurePathAbs;
7069
private final InclusionStrategy mergeStrategy;
70+
private final ModuleLoader loader;
7171
final ErrorManager errorManager;
7272

7373
static final DiagnosticType SAME_FILE_WARNING = DiagnosticType.warning(
@@ -102,12 +102,14 @@ public DepsGenerator(
102102
Collection<SourceFile> srcs,
103103
InclusionStrategy mergeStrategy,
104104
String closurePathAbs,
105-
ErrorManager errorManager) {
105+
ErrorManager errorManager,
106+
ModuleLoader loader) {
106107
this.deps = deps;
107108
this.srcs = srcs;
108109
this.mergeStrategy = mergeStrategy;
109110
this.closurePathAbs = closurePathAbs;
110111
this.errorManager = errorManager;
112+
this.loader = loader;
111113
}
112114

113115
/**
@@ -323,7 +325,7 @@ private Map<String, DependencyInfo> parseDepsFiles() throws IOException {
323325
private Map<String, DependencyInfo> parseSources(
324326
Set<String> preparsedFiles) throws IOException {
325327
Map<String, DependencyInfo> parsedFiles = new HashMap<>();
326-
JsFileParser jsParser = new JsFileParser(errorManager);
328+
JsFileParser jsParser = new JsFileParser(errorManager).setModuleLoader(loader);
327329
Compiler compiler = new Compiler();
328330
compiler.init(
329331
ImmutableList.<SourceFile>of(), ImmutableList.<SourceFile>of(), new CompilerOptions());

src/com/google/javascript/jscomp/deps/Es6SortedDependencies.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818

1919
import com.google.common.base.Preconditions;
2020
import com.google.common.collect.ImmutableList;
21+
import com.google.common.collect.Iterables;
2122
import com.google.common.collect.LinkedHashMultimap;
2223
import com.google.common.collect.Multimap;
23-
2424
import java.util.ArrayDeque;
2525
import java.util.ArrayList;
26+
import java.util.Collection;
2627
import java.util.Collections;
2728
import java.util.Deque;
2829
import java.util.HashMap;
@@ -142,7 +143,16 @@ private void orderInput(INPUT input) {
142143
private void processInputs() {
143144
// Index.
144145
for (INPUT userOrderedInput : userOrderedInputs) {
145-
if (userOrderedInput.getProvides().isEmpty()) {
146+
Collection<String> provides = userOrderedInput.getProvides();
147+
String firstProvide = Iterables.getFirst(provides, null);
148+
if (firstProvide == null
149+
// TODO(sdh): It would be better to have a more robust way to distinguish
150+
// between actual provided symbols and synthetic symbols generated for
151+
// ES6 (or other) modules. We can't read loadFlags here (to see if
152+
// the module type is 'es6') either, since that requires a full parse.
153+
// So for now we rely on the heuristic that all generated provides start
154+
// with "module$".
155+
|| (provides.size() == 1 && firstProvide.startsWith("module$"))) {
146156
nonExportingInputs.put(
147157
ModuleNames.fileToModuleName(userOrderedInput.getName()), userOrderedInput);
148158
}

src/com/google/javascript/jscomp/deps/JsFileParser.java

Lines changed: 111 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,51 +18,91 @@
1818

1919
import com.google.common.annotations.GwtIncompatible;
2020
import com.google.common.base.CharMatcher;
21+
import com.google.javascript.jscomp.CheckLevel;
2122
import com.google.javascript.jscomp.ErrorManager;
22-
23+
import com.google.javascript.jscomp.JSError;
2324
import java.io.Reader;
2425
import java.io.StringReader;
2526
import java.util.ArrayList;
27+
import java.util.LinkedHashMap;
2628
import java.util.List;
29+
import java.util.Map;
2730
import java.util.logging.Logger;
2831
import java.util.regex.Matcher;
2932
import java.util.regex.Pattern;
3033

3134
/**
32-
* A parser that can extract goog.require() and goog.provide() dependency
33-
* information from a .js file.
35+
* A parser that can extract dependency information from a .js file, including
36+
* goog.require, goog.provide, goog.module, import statements, and export statements.
3437
*
3538
* @author [email protected] (Andrew Grieve)
3639
*/
3740
@GwtIncompatible("java.util.regex")
3841
public final class JsFileParser extends JsFileLineParser {
3942

40-
private static Logger logger = Logger.getLogger(JsFileParser.class.getName());
43+
private static final Logger logger = Logger.getLogger(JsFileParser.class.getName());
4144

4245
/** Pattern for matching goog.provide(*) and goog.require(*). */
4346
private static final Pattern GOOG_PROVIDE_REQUIRE_PATTERN =
4447
Pattern.compile(
4548
"(?:^|;)(?:[a-zA-Z0-9$_,:{}\\s]+=)?\\s*"
4649
+ "goog\\.(provide|module|require|addDependency)\\s*\\((.*?)\\)");
4750

51+
/**
52+
* Pattern for matching import ... from './path/to/file'.
53+
*
54+
* <p>Unlike the goog.require() pattern above, this pattern does not
55+
* allow multiple statements per line. The import/export <b>must</b>
56+
* be at the beginning of the line to match.
57+
*/
58+
private static final Pattern ES6_MODULE_PATTERN =
59+
Pattern.compile(
60+
// Require the import/export to be at the beginning of the line
61+
"^"
62+
// Either an import or export, but we don't care which, followed by at least one space
63+
+ "(?:import|export)\\b\\s*"
64+
// Skip any identifier chars, as well as star, comma, braces, and spaces
65+
// This should match, e.g., "* as foo from ", or "Foo, {Bar as Baz} from ".
66+
// The 'from' keyword is required except in the case of "import '...';",
67+
// where there's nothing between 'import' and the module key string literal.
68+
+ "(?:[a-zA-Z0-9$_*,{}\\s]+\\bfrom\\s*|)"
69+
// Imports require a string literal at the end; it's optional for exports
70+
// (e.g. "export * from './other';", which is effectively also an import).
71+
// This optionally captures group #1, which is the imported module name.
72+
+ "(?:['\"]([^'\"]+)['\"])?"
73+
// Finally, this should be the entire statement, so ensure there's a semicolon.
74+
+ "\\s*;");
75+
76+
/**
77+
* Pattern for 'export' keyword, e.g. "export default class ..." or "export {blah}".
78+
* The '\b' ensures we don't also match "exports = ...", which is not an ES6 module.
79+
*/
80+
private static final Pattern ES6_EXPORT_PATTERN = Pattern.compile("^export\\b");
81+
4882
/** The first non-comment line of base.js */
4983
private static final String BASE_JS_START = "var COMPILED = false;";
5084

5185
/** The start of a bundled goog.module, i.e. one that is wrapped in a goog.loadModule call */
5286
private static final String BUNDLED_GOOG_MODULE_START = "goog.loadModule(function(";
5387

5488
/** Matchers used in the parsing. */
55-
private Matcher googMatcher = GOOG_PROVIDE_REQUIRE_PATTERN.matcher("");
89+
private final Matcher googMatcher = GOOG_PROVIDE_REQUIRE_PATTERN.matcher("");
90+
91+
/** Matchers used in the parsing. */
92+
private final Matcher es6Matcher = ES6_MODULE_PATTERN.matcher("");
5693

5794
/** The info for the file we are currently parsing. */
5895
private List<String> provides;
5996
private List<String> requires;
6097
private boolean fileHasProvidesOrRequires;
98+
private ModuleLoader loader = ModuleLoader.EMPTY;
99+
private ModuleLoader.ModuleUri fileUri;
61100

62101
private enum ModuleType {
63102
NON_MODULE,
64103
UNWRAPPED_GOOG_MODULE,
65104
WRAPPED_GOOG_MODULE,
105+
ES6_MODULE,
66106
}
67107

68108
private ModuleType moduleType;
@@ -97,6 +137,17 @@ public JsFileParser setIncludeGoogBase(boolean include) {
97137
return this;
98138
}
99139

140+
/**
141+
* Sets a list of "module root" URIs, which allow relativizing filenames
142+
* for modules.
143+
*
144+
* @return this for easy chaining.
145+
*/
146+
public JsFileParser setModuleLoader(ModuleLoader loader) {
147+
this.loader = loader;
148+
return this;
149+
}
150+
100151
/**
101152
* Parses the given file and returns the dependency information that it
102153
* contained.
@@ -115,21 +166,46 @@ public DependencyInfo parseFile(String filePath, String closureRelativePath,
115166

116167
private DependencyInfo parseReader(String filePath,
117168
String closureRelativePath, Reader fileContents) {
118-
provides = new ArrayList<>();
119-
requires = new ArrayList<>();
120-
fileHasProvidesOrRequires = false;
121-
moduleType = ModuleType.NON_MODULE;
169+
this.provides = new ArrayList<>();
170+
this.requires = new ArrayList<>();
171+
this.fileHasProvidesOrRequires = false;
172+
this.fileUri = loader.resolve(filePath);
173+
this.moduleType = ModuleType.NON_MODULE;
122174

123175
logger.fine("Parsing Source: " + filePath);
124176
doParse(filePath, fileContents);
125177

178+
if (moduleType == ModuleType.ES6_MODULE) {
179+
provides.add(fileUri.toModuleName());
180+
}
181+
182+
Map<String, String> loadFlags = new LinkedHashMap<>();
183+
switch (moduleType) {
184+
case UNWRAPPED_GOOG_MODULE:
185+
loadFlags.put("module", "goog");
186+
break;
187+
case ES6_MODULE:
188+
loadFlags.put("module", "es6");
189+
break;
190+
default:
191+
// Nothing to do here.
192+
}
193+
126194
DependencyInfo dependencyInfo = new SimpleDependencyInfo(
127-
closureRelativePath, filePath, provides, requires,
128-
moduleType == ModuleType.UNWRAPPED_GOOG_MODULE);
195+
closureRelativePath, filePath, provides, requires, loadFlags);
129196
logger.fine("DepInfo: " + dependencyInfo);
130197
return dependencyInfo;
131198
}
132199

200+
private void setModuleType(ModuleType type) {
201+
if (moduleType != type && moduleType != ModuleType.NON_MODULE) {
202+
// TODO(sdh): should this be an error?
203+
errorManager.report(
204+
CheckLevel.WARNING, JSError.make(ModuleLoader.MODULE_CONFLICT, fileUri.toString()));
205+
}
206+
moduleType = type;
207+
}
208+
133209
/**
134210
* Parses a line of JavaScript, extracting goog.provide and goog.require
135211
* information.
@@ -161,7 +237,7 @@ protected boolean parseLine(String line) throws ParseException {
161237
boolean isRequire = firstChar == 'r';
162238

163239
if (isModule && this.moduleType != ModuleType.WRAPPED_GOOG_MODULE) {
164-
this.moduleType = ModuleType.UNWRAPPED_GOOG_MODULE;
240+
setModuleType(ModuleType.UNWRAPPED_GOOG_MODULE);
165241
}
166242

167243
if (isProvide || isRequire) {
@@ -187,7 +263,29 @@ protected boolean parseLine(String line) throws ParseException {
187263
// base.js can't provide or require anything else.
188264
return false;
189265
} else if (line.startsWith(BUNDLED_GOOG_MODULE_START)) {
190-
this.moduleType = ModuleType.WRAPPED_GOOG_MODULE;
266+
setModuleType(ModuleType.WRAPPED_GOOG_MODULE);
267+
}
268+
269+
if (line.startsWith("import") || line.startsWith("export")) {
270+
es6Matcher.reset(line);
271+
while (es6Matcher.find()) {
272+
setModuleType(ModuleType.ES6_MODULE);
273+
lineHasProvidesOrRequires = true;
274+
275+
String arg = es6Matcher.group(1);
276+
if (arg != null) {
277+
if (arg.startsWith("goog:")) {
278+
requires.add(arg.substring(5)); // cut off the "goog:" prefix
279+
} else {
280+
requires.add(fileUri.resolveEs6Module(arg).toModuleName());
281+
}
282+
}
283+
}
284+
285+
// This check is only relevant for modules that don't import anything.
286+
if (moduleType != ModuleType.ES6_MODULE && ES6_EXPORT_PATTERN.matcher(line).lookingAt()) {
287+
setModuleType(ModuleType.ES6_MODULE);
288+
}
191289
}
192290

193291
return !shortcutMode || lineHasProvidesOrRequires

0 commit comments

Comments
 (0)