Skip to content

Commit fdda0c3

Browse files
lauraharkercopybara-github
authored andcommitted
Warn on a goog.module.get without a goog.require in the top-level of a script
Normally the compiler does not enforce goog.requires for goog.module.get, as they are allowed as a way to break dependency cycles between files: a goog.module.get is acceptable if within some function body, that may not be executed until the retrieved module is later loaded. However, when a goog.module.get executes immediately upon script load (so before any other provides/modules can load), it does need a goog.require. PiperOrigin-RevId: 728717544
1 parent 02529ad commit fdda0c3

File tree

2 files changed

+266
-1
lines changed

2 files changed

+266
-1
lines changed

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

+91-1
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,26 @@ public class CheckMissingRequires extends AbstractModuleCallback implements Comp
9191
"''{0}'' references the name of a module without goog.declareLegacyNamespace(), which "
9292
+ "is not actually defined. Use goog.module.get() instead.");
9393

94+
public static final DiagnosticType MISSING_REQUIRE_FOR_GOOG_MODULE_GET =
95+
DiagnosticType.warning(
96+
"JSC_MISSING_REQUIRE_FOR_GOOG_MODULE_GET",
97+
"''{0}'' references a namespace which was not required by this file.\n"
98+
+ "Please add a goog.require.");
99+
94100
/** The set of template parameter names found so far in the file currently being checked. */
95101
private final LinkedHashSet<String> templateParamNames = new LinkedHashSet<>();
96102

97103
/** The mapping from Closure namespace into the module that provides it. */
98104
private final ImmutableMap<String, ModuleMetadata> moduleByNamespace;
99105

106+
/**
107+
* Tracks how many "control flow scopes" we've entered, starting at 0
108+
*
109+
* <p>Where a "control flow scope" is here defined as any scope /except/ for either the global
110+
* control flow scope, or an IIFE or goog.scope body within the global control flow scope.
111+
*/
112+
private int controlFlowScopeDepth = 0;
113+
100114
public CheckMissingRequires(AbstractCompiler compiler, ModuleMetadataMap moduleMetadataMap) {
101115
super(compiler, moduleMetadataMap);
102116
this.moduleByNamespace = moduleMetadataMap.getModulesByGoogNamespace();
@@ -110,6 +124,9 @@ public void process(Node externs, Node root) {
110124
@Override
111125
public boolean shouldTraverse(
112126
NodeTraversal t, Node n, @Nullable ModuleMetadata currentModule, Node scopeRoot) {
127+
if (NodeUtil.isValidCfgRoot(n) && !isInGlobalControlFlowScope(n)) {
128+
controlFlowScopeDepth++;
129+
}
113130
if (currentModule == null) {
114131
return true;
115132
}
@@ -119,9 +136,43 @@ public boolean shouldTraverse(
119136
return true;
120137
}
121138

139+
/**
140+
* Takes a valid "control flow root", as defined by {@link NodeUtil#isValidCfgRoot}, and returns
141+
* whether it creates a new "control flow scope" from the parent scope (the global scope).
142+
*/
143+
private boolean isInGlobalControlFlowScope(Node n) {
144+
switch (n.getToken()) {
145+
case SCRIPT:
146+
case ROOT:
147+
return true;
148+
case MODULE_BODY:
149+
case BLOCK: // class static blocks
150+
return false;
151+
case FUNCTION:
152+
if (controlFlowScopeDepth > 0) {
153+
return false;
154+
}
155+
// Check for functions invoked immediately within the global scope - so IIFEs or
156+
// goog.scope callees.
157+
if (isGoogScopeBody(NodeUtil.getFunctionBody(n))) {
158+
return true;
159+
}
160+
return isIIFE(n);
161+
default:
162+
throw new AssertionError("Unexpected control flow root: " + n);
163+
}
164+
}
165+
166+
private static boolean isIIFE(Node n) {
167+
return n.isFunction() && n.getParent().isCall() && n.isFirstChildOf(n.getParent());
168+
}
169+
122170
@Override
123171
public void visit(
124172
NodeTraversal t, Node n, @Nullable ModuleMetadata currentModule, @Nullable Node scopeRoot) {
173+
if (NodeUtil.isValidCfgRoot(n) && !isInGlobalControlFlowScope(n)) {
174+
controlFlowScopeDepth--;
175+
}
125176
if (currentModule != null && n == currentModule.rootNode()) {
126177
// For this pass, template parameter names are only meaningful inside the file defining them.
127178
templateParamNames.clear();
@@ -145,6 +196,10 @@ private void visitNode(NodeTraversal t, Node n, ModuleMetadata currentModule) {
145196
if (n.isName() && !n.getString().isEmpty()) {
146197
visitMaybeDeclaration(t, n, currentModule);
147198
}
199+
200+
if (n.isCall() && controlFlowScopeDepth == 0) {
201+
visitMaybeGoogModuleGet(t, n, currentModule);
202+
}
148203
}
149204

150205
private void visitJsDocInfo(NodeTraversal t, ModuleMetadata currentModule, JSDocInfo info) {
@@ -197,9 +252,44 @@ private void visitJsDocExpr(
197252
}
198253
}
199254

255+
private static final QualifiedName GOOG_SCOPE = QualifiedName.of("goog.scope");
256+
257+
private static boolean isGoogScopeBody(Node hoistScopeRoot) {
258+
return hoistScopeRoot.isBlock()
259+
&& hoistScopeRoot.getParent().isFunction()
260+
&& hoistScopeRoot.getGrandparent().isCall()
261+
&& GOOG_SCOPE.matches(hoistScopeRoot.getGrandparent().getFirstChild());
262+
}
263+
264+
/**
265+
* Check for invalid goog.module.get calls executed on script load
266+
*
267+
* <p>We don't check for goog.module.get within function bodies though (except for goog.scope
268+
* since that's basically an IIFE) - it's possible that the module will have been loaded by the
269+
* time the goog.module.get is called even though there's no strong require. It's up to the caller
270+
* to ensure that it's loaded.
271+
*/
272+
private void visitMaybeGoogModuleGet(
273+
NodeTraversal t, Node googModuleGet, ModuleMetadata currentFile) {
274+
checkState(googModuleGet.isCall());
275+
if (!NodeUtil.isGoogModuleGetCall(googModuleGet)) {
276+
return;
277+
}
278+
String importedNamespace = googModuleGet.getSecondChild().getString();
279+
ModuleMetadata requiredFile = moduleByNamespace.get(importedNamespace);
280+
if (requiredFile == null) {
281+
// goog.module.get of a non-existing namespace is an error in another pass.
282+
return;
283+
}
284+
if (!hasAcceptableRequire(
285+
currentFile, QualifiedName.of(importedNamespace), requiredFile, Strength.WEAK_TYPE)) {
286+
t.report(googModuleGet, MISSING_REQUIRE_FOR_GOOG_MODULE_GET, importedNamespace);
287+
}
288+
}
289+
200290
private void visitMaybeDeclaration(NodeTraversal t, Node n, ModuleMetadata currentFile) {
201291
if (!currentFile.isModule()) {
202-
// This check only makes sense in goog.module files.
292+
// This check only makes sense in goog.module files
203293
return;
204294
}
205295

test/com/google/javascript/jscomp/CheckMissingRequiresTest.java

+175
Original file line numberDiff line numberDiff line change
@@ -1310,6 +1310,174 @@ public void tesNonLegacyGoogModule_inScript_required_noWarning() {
13101310
"goog.provide('test.b'); goog.require('test.a'); console.log(test.a.x);");
13111311
}
13121312

1313+
@Test
1314+
public void testWarning_googScope_googModuleGet_noRequire() {
1315+
checkRequireForGoogModuleGetWarning(
1316+
"foo.bar.Baz",
1317+
lines(
1318+
"goog.module('foo.bar.Baz');", //
1319+
"/** @constructor */",
1320+
"exports = function() {}"),
1321+
lines(
1322+
"goog.provide('test');", //
1323+
"",
1324+
"goog.scope(function() {",
1325+
"const Baz = goog.module.get('foo.bar.Baz');",
1326+
"exports.fn = function() { new Baz(); }",
1327+
"});"));
1328+
}
1329+
1330+
@Test
1331+
public void testWarning_script_googModuleGet_inProvideInitialization_noRequire() {
1332+
checkRequireForGoogModuleGetWarning(
1333+
"foo.bar.Baz",
1334+
lines(
1335+
"goog.module('foo.bar.Baz');", //
1336+
"/** @constructor */",
1337+
"exports.Boo = function() {}"),
1338+
lines(
1339+
"goog.provide('test');", //
1340+
"",
1341+
"test.boo = new (goog.module.get('foo.bar.Baz').Boo)();"));
1342+
}
1343+
1344+
@Test
1345+
public void testWarning_script_googModuleGet_inScriptIfBlock_noRequire() {
1346+
checkRequireForGoogModuleGetWarning(
1347+
"foo.bar.Baz",
1348+
lines(
1349+
"goog.module('foo.bar.Baz');", //
1350+
"/** @constructor */",
1351+
"exports.Boo = function() {}"),
1352+
lines(
1353+
"goog.provide('test');", //
1354+
"",
1355+
"if (true) { console.log(new (goog.module.get('foo.bar.Baz').Boo)()); }"));
1356+
}
1357+
1358+
@Test
1359+
public void testWarning_script_googModuleGet_inScript_inTopLevelIIFE_noRequire() {
1360+
checkRequireForGoogModuleGetWarning(
1361+
"foo.bar.Baz",
1362+
lines(
1363+
"goog.module('foo.bar.Baz');", //
1364+
"/** @constructor */",
1365+
"exports.Boo = function() {}"),
1366+
lines(
1367+
"goog.provide('test');", //
1368+
"",
1369+
"(() => console.log(new (goog.module.get('foo.bar.Baz').Boo)()))();"));
1370+
}
1371+
1372+
@Test
1373+
public void testWarning_googScope_googModuleGet_withPropAccess_noRequire() {
1374+
checkRequireForGoogModuleGetWarning(
1375+
"foo.bar.Baz",
1376+
lines(
1377+
"goog.module('foo.bar.Baz');", //
1378+
"/** @constructor */",
1379+
"exports.Boo = function() {}"),
1380+
lines(
1381+
"goog.provide('test');", //
1382+
"",
1383+
"goog.scope(function() {",
1384+
"const Woo = goog.module.get('foo.bar.Baz').Boo.Goo.Woo;",
1385+
"exports.fn = function() { new Woo(); }",
1386+
"});"));
1387+
}
1388+
1389+
@Test
1390+
public void testNoWarning_googScope_googModuleGet_hasRequire() {
1391+
checkNoWarning(
1392+
lines(
1393+
"goog.module('foo.bar.Baz');", //
1394+
"/** @constructor */",
1395+
"exports = function() {}"),
1396+
lines(
1397+
"goog.provide('test');", //
1398+
"goog.require('foo.bar.Baz');",
1399+
"",
1400+
"goog.scope(function() {",
1401+
"const Baz = goog.module.get('foo.bar.Baz');",
1402+
"function fn() { new Baz(); }",
1403+
"});"));
1404+
}
1405+
1406+
@Test
1407+
public void testWarning_googScope_googModuleGet_IIFE_noRequire() {
1408+
checkRequireForGoogModuleGetWarning(
1409+
"foo.bar.Baz",
1410+
lines(
1411+
"goog.module('foo.bar.Baz');", //
1412+
"/** @constructor */",
1413+
"exports.Boo = function() {}"),
1414+
lines(
1415+
"goog.provide('test');", //
1416+
"",
1417+
"goog.scope(function() {",
1418+
" (() => console.log(new (goog.module.get('foo.bar.Baz').Boo)()))();",
1419+
"});"));
1420+
}
1421+
1422+
@Test
1423+
public void testWarning_script_googModuleGet_inFunctionInScript_noRequire_noWarning() {
1424+
checkNoWarning(
1425+
lines(
1426+
"goog.module('foo.bar.Baz');", //
1427+
"/** @constructor */",
1428+
"exports.Boo = function() {}"),
1429+
lines(
1430+
"goog.provide('test');", //
1431+
"",
1432+
// We allow this goog.module.get despite the lack of goog.require: it's possible that
1433+
// the foo.bar.Baz module will have been loaded by the time test.fn is called. It's up
1434+
// to the caller to ensure that it's loaded.
1435+
"test.fn = function() {",
1436+
" console.log(new (goog.module.get('foo.bar.Baz').Boo)());",
1437+
"}"));
1438+
}
1439+
1440+
@Test
1441+
public void
1442+
testWarning_script_googModuleGet_inFunctionInScript_inNonTopLevelIIFE_noRequire_noWarning() {
1443+
checkNoWarning(
1444+
lines(
1445+
"goog.module('foo.bar.Baz');", //
1446+
"/** @constructor */",
1447+
"exports.Boo = function() {}"),
1448+
lines(
1449+
"goog.provide('test');", //
1450+
"",
1451+
// We allow this goog.module.get despite the lack of goog.require: it's possible that
1452+
// the foo.bar.Baz module will have been loaded by the time test.fn is called. It's up
1453+
// to the caller to ensure that it's loaded.
1454+
"test.fn = function() {",
1455+
" (() => console.log(new (goog.module.get('foo.bar.Baz').Boo)()))();",
1456+
"}"));
1457+
}
1458+
1459+
@Test
1460+
public void testNoWarning_nestedInGoogScope_googModuleGet_noRequire() {
1461+
checkNoWarning(
1462+
lines(
1463+
"goog.module('foo.bar.Baz');", //
1464+
"/** @constructor */",
1465+
"exports = function() {}"),
1466+
lines(
1467+
"goog.provide('test');", //
1468+
"",
1469+
"goog.scope(function() {",
1470+
"test.fn = function() {",
1471+
// We allow this goog.module.get despite the lack of goog.require: it's possible that
1472+
// the foo.bar.Baz module will have been loaded by the time test.fn is called. It's up
1473+
// to the caller to ensure that it's loaded.
1474+
" const Baz = goog.module.get('foo.bar.Baz');",
1475+
" new Baz();",
1476+
" new goog.module.get('foo.bar.Baz');",
1477+
"}",
1478+
"});"));
1479+
}
1480+
13131481
private void checkNoWarning(String... js) {
13141482
testSame(srcs(js));
13151483
}
@@ -1368,4 +1536,11 @@ private void checkIncorrectNamespaceAliasRequireTypeWarning(String namespace, St
13681536
warning(CheckMissingRequires.INCORRECT_NAMESPACE_ALIAS_REQUIRE_TYPE)
13691537
.withMessageContaining("'" + namespace + "'"));
13701538
}
1539+
1540+
private void checkRequireForGoogModuleGetWarning(String namespace, String... js) {
1541+
testSame(
1542+
srcs(js),
1543+
warning(CheckMissingRequires.MISSING_REQUIRE_FOR_GOOG_MODULE_GET)
1544+
.withMessageContaining("'" + namespace + "'"));
1545+
}
13711546
}

0 commit comments

Comments
 (0)