Skip to content

Commit c0193c4

Browse files
lauraharkercopybara-github
authored andcommitted
Add NON_LEGACY_GOOG_MODULE_REFERENCE warning to CheckMissingRequires.
This warnings on direct references of a goog.module namespace without goog.module.declareLegacyNamespace() in code. This pattern already does not work at runtime - without out goog.module.declareLegacyNamespace(), the name of a goog.module is just an opaque identifier in code. This warning should provide stronger guarantees than the existing type system "inexistent property" errors, which are not always reliable. Referencing a goog.module namespace in a type annotation still is allowed in scripts. PiperOrigin-RevId: 726613351
1 parent 97b3190 commit c0193c4

File tree

2 files changed

+121
-27
lines changed

2 files changed

+121
-27
lines changed

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

+65-27
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ public class CheckMissingRequires extends AbstractModuleCallback implements Comp
8585
"''{0}'' references a namespace which was not required by this file.\n"
8686
+ "Please add a goog.requireType.");
8787

88+
public static final DiagnosticType NON_LEGACY_GOOG_MODULE_REFERENCE =
89+
DiagnosticType.error(
90+
"JSC_NON_LEGACY_GOOG_MODULE_REFERENCE",
91+
"''{0}'' references the name of a module without goog.declareLegacyNamespace(), which "
92+
+ "is not actually defined. Use goog.module.get() instead.");
93+
8894
/** The set of template parameter names found so far in the file currently being checked. */
8995
private final LinkedHashSet<String> templateParamNames = new LinkedHashSet<>();
9096

@@ -133,7 +139,7 @@ private void visitNode(NodeTraversal t, Node n, ModuleMetadata currentModule) {
133139
if (root.equals("this") || root.equals("super")) {
134140
return;
135141
}
136-
visitQualifiedName(t, n, currentModule, qualifiedName, /* isStrongReference= */ true);
142+
visitQualifiedName(t, n, currentModule, qualifiedName, Strength.CODE);
137143
}
138144

139145
if (n.isName() && !n.getString().isEmpty()) {
@@ -147,48 +153,47 @@ private void visitJsDocInfo(NodeTraversal t, ModuleMetadata currentModule, JSDoc
147153
templateParamNames.addAll(info.getTemplateTypeNames());
148154
templateParamNames.addAll(info.getTypeTransformations().keySet());
149155
if (info.hasType()) {
150-
visitJsDocExpr(t, currentModule, info.getType(), /* isStrongReference= */ false);
156+
visitJsDocExpr(t, currentModule, info.getType(), Strength.WEAK_TYPE);
151157
}
152158
for (String param : info.getParameterNames()) {
153159
if (info.hasParameterType(param)) {
154-
visitJsDocExpr(
155-
t, currentModule, info.getParameterType(param), /* isStrongReference= */ false);
160+
visitJsDocExpr(t, currentModule, info.getParameterType(param), Strength.WEAK_TYPE);
156161
}
157162
}
158163
if (info.hasReturnType()) {
159-
visitJsDocExpr(t, currentModule, info.getReturnType(), /* isStrongReference= */ false);
164+
visitJsDocExpr(t, currentModule, info.getReturnType(), Strength.WEAK_TYPE);
160165
}
161166
if (info.hasEnumParameterType()) {
162-
visitJsDocExpr(t, currentModule, info.getEnumParameterType(), /* isStrongReference= */ false);
167+
visitJsDocExpr(t, currentModule, info.getEnumParameterType(), Strength.WEAK_TYPE);
163168
}
164169
if (info.hasTypedefType()) {
165-
visitJsDocExpr(t, currentModule, info.getTypedefType(), /* isStrongReference= */ false);
170+
visitJsDocExpr(t, currentModule, info.getTypedefType(), Strength.WEAK_TYPE);
166171
}
167172
if (info.hasThisType()) {
168-
visitJsDocExpr(t, currentModule, info.getThisType(), /* isStrongReference= */ false);
173+
visitJsDocExpr(t, currentModule, info.getThisType(), Strength.WEAK_TYPE);
169174
}
170175
if (info.hasBaseType()) {
171176
// Note that `@extends` requires a goog.require, not a goog.requireType.
172-
visitJsDocExpr(t, currentModule, info.getBaseType(), /* isStrongReference= */ true);
177+
visitJsDocExpr(t, currentModule, info.getBaseType(), Strength.IMPLEMENTS_EXTENDS);
173178
}
174179
for (JSTypeExpression expr : info.getExtendedInterfaces()) {
175180
// Note that `@extends` requires a goog.require, not a goog.requireType.
176-
visitJsDocExpr(t, currentModule, expr, /* isStrongReference= */ true);
181+
visitJsDocExpr(t, currentModule, expr, Strength.IMPLEMENTS_EXTENDS);
177182
}
178183
for (JSTypeExpression expr : info.getImplementedInterfaces()) {
179184
// Note that `@implements` requires a goog.require, not a goog.requireType.
180-
visitJsDocExpr(t, currentModule, expr, /* isStrongReference= */ true);
185+
visitJsDocExpr(t, currentModule, expr, Strength.IMPLEMENTS_EXTENDS);
181186
}
182187
}
183188

184189
private void visitJsDocExpr(
185190
NodeTraversal t,
186191
ModuleMetadata currentModule,
187192
JSTypeExpression expr,
188-
boolean isStrongReference) {
193+
Strength referenceStrength) {
189194
for (Node typeNode : expr.getAllTypeNodes()) {
190195
visitQualifiedName(
191-
t, typeNode, currentModule, QualifiedName.of(typeNode.getString()), isStrongReference);
196+
t, typeNode, currentModule, QualifiedName.of(typeNode.getString()), referenceStrength);
192197
}
193198
}
194199

@@ -258,7 +263,10 @@ private void visitMaybeDeclaration(NodeTraversal t, Node n, ModuleMetadata curre
258263
// through namespace destructuring, otherwise...
259264
if (alternateFile != null && alternateFile.hasLegacyGoogNamespaces()) {
260265
if (!hasAcceptableRequire(
261-
currentFile, qualifiedName, alternateFile, require.isStrongRequire())) {
266+
currentFile,
267+
qualifiedName,
268+
alternateFile,
269+
require.isStrongRequire() ? Strength.CODE : Strength.WEAK_TYPE)) {
262270
// TODO: report on the node that needs to be removed, include the namespace that needs
263271
// to be added.
264272
final DiagnosticType toReport =
@@ -281,7 +289,7 @@ private void visitQualifiedName(
281289
Node n,
282290
ModuleMetadata currentFile,
283291
QualifiedName qualifiedName,
284-
boolean isStrongReference) {
292+
Strength referenceStrength) {
285293

286294
String rootName = qualifiedName.getRoot();
287295
if (qualifiedName.isSimple()) {
@@ -301,10 +309,10 @@ private void visitQualifiedName(
301309

302310
Var var = t.getScope().getVar(rootName);
303311
if (var != null && var.getScope().isLocal()) {
304-
checkMissingRequireThroughShortName(t, n, var, currentFile, qualifiedName, isStrongReference);
312+
checkMissingRequireThroughShortName(t, n, var, currentFile, qualifiedName, referenceStrength);
305313
} else {
306314
checkMissingRequireThroughFullyQualifiedName(
307-
t, n, currentFile, qualifiedName, isStrongReference);
315+
t, n, currentFile, qualifiedName, referenceStrength);
308316
}
309317
}
310318

@@ -325,14 +333,15 @@ private void checkMissingRequireThroughShortName(
325333
Var localVar,
326334
ModuleMetadata currentFile,
327335
QualifiedName qualifiedName,
328-
boolean isStrongReference) {
336+
Strength referenceStrength) {
329337
// TODO(b/333952917): Remove this once tsickle is fixed.
330338
if (isTypeScriptSource(n)) {
331339
return;
332340
}
333341

334342
if (!currentFile.isModule()) {
335343
// Don't worry about aliases outside of module files for now.
344+
// TODO - b/390433455: also check aliases within the top level of a goog.scope file.
336345
return;
337346
}
338347

@@ -392,14 +401,14 @@ private void checkMissingRequireThroughShortName(
392401
// fix
393402
// it up. Write a different message if the namespace is already imported vs missing.
394403

395-
if (!hasAcceptableRequire(currentFile, subName, requiredFile, isStrongReference)
404+
if (!hasAcceptableRequire(currentFile, subName, requiredFile, referenceStrength)
396405
|| originalRequiredFile != requiredFile) {
397406

398407
// `originalRequiredFile != requiredFile` then the file is being referenced through
399408
// the wrong namespace even though it is being `goog.require`d in the file.
400409

401410
DiagnosticType toReport =
402-
isStrongReference
411+
referenceStrength.isStrong
403412
? INDIRECT_NAMESPACE_REF_REQUIRE
404413
: INDIRECT_NAMESPACE_REF_REQUIRE_TYPE;
405414
t.report(n, toReport, namespace);
@@ -418,14 +427,14 @@ private void checkMissingRequireThroughShortName(
418427
*
419428
* @param n the node representing the fully qualified name being checked
420429
* @param qualifiedName some global fully qualified name to check
421-
* @param isStrongReference whether this is a non-type-only reference
430+
* @param referenceStrength whether this is a non-type-only reference
422431
*/
423432
private void checkMissingRequireThroughFullyQualifiedName(
424433
NodeTraversal t,
425434
Node n,
426435
ModuleMetadata currentFile,
427436
QualifiedName qualifiedName,
428-
boolean isStrongReference) {
437+
Strength referenceStrength) {
429438

430439
// Look for the longest prefix match against a provided namespace.
431440
for (QualifiedName subName = qualifiedName; subName != null; subName = subName.getOwner()) {
@@ -446,16 +455,23 @@ private void checkMissingRequireThroughFullyQualifiedName(
446455
* In files that represent modules, report a require without an alias the same as a totally
447456
* missing require.
448457
*/
449-
toReport = isStrongReference ? MISSING_REQUIRE : MISSING_REQUIRE_TYPE;
450-
} else if (!hasAcceptableRequire(currentFile, subName, requiredFile, isStrongReference)) {
458+
toReport = referenceStrength.isStrong ? MISSING_REQUIRE : MISSING_REQUIRE_TYPE;
459+
} else if (!hasAcceptableRequire(currentFile, subName, requiredFile, referenceStrength)) {
451460
/*
452461
* In files that aren't modules, report a qualified name reference only if there's no
453462
* require to satisfy it.
454463
*/
455464
toReport =
456-
isStrongReference
465+
referenceStrength.isStrong
457466
? MISSING_REQUIRE_IN_PROVIDES_FILE
458467
: MISSING_REQUIRE_TYPE_IN_PROVIDES_FILE;
468+
} else if (!referenceStrength.isTypeOnly && requiredFile.isNonLegacyGoogModule()) {
469+
// The referenced file has a valid goog.require for it, but the reference cannot be in a
470+
// regular qualified name: that won't be defined at runtime.
471+
// However, this only applies to references in actual code. Type-only references may still
472+
// reference the namespace in a script. The Closure type system will still resolve the
473+
// namespace - it just doesn't have any runtime effect.
474+
toReport = NON_LEGACY_GOOG_MODULE_REFERENCE;
459475
} else {
460476
return;
461477
}
@@ -515,9 +531,12 @@ boolean isAllowedNamespace(ModuleMetadata currentFile, String namespace) {
515531
* different files.
516532
*/
517533
private static boolean hasAcceptableRequire(
518-
ModuleMetadata rdep, QualifiedName namespace, ModuleMetadata dep, boolean isStrongReference) {
534+
ModuleMetadata rdep,
535+
QualifiedName namespace,
536+
ModuleMetadata dep,
537+
Strength referenceStrength) {
519538
Set<String> acceptableRequires = rdep.stronglyRequiredGoogNamespaces().elementSet();
520-
if (!isStrongReference) {
539+
if (!referenceStrength.isStrong) {
521540
acceptableRequires =
522541
Sets.union(acceptableRequires, rdep.weaklyRequiredGoogNamespaces().elementSet());
523542
}
@@ -531,4 +550,23 @@ private static boolean hasAcceptableRequire(
531550

532551
return false;
533552
}
553+
554+
/**
555+
* Represents the strength of references of an require'd name, i.e. whether references are in code
556+
* only (strong, not-typeOnly), JsDoc annotations for \@implements or \@extends (strong as well as
557+
* typeOnly) or in other JsDoc annotations only (weak, typeOnly).
558+
*/
559+
enum Strength {
560+
CODE(true, false),
561+
IMPLEMENTS_EXTENDS(true, true),
562+
WEAK_TYPE(false, true);
563+
564+
Strength(boolean isStrong, boolean isTypeOnly) {
565+
this.isStrong = isStrong;
566+
this.isTypeOnly = isTypeOnly;
567+
}
568+
569+
final boolean isStrong;
570+
final boolean isTypeOnly;
571+
}
534572
}

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

+56
Original file line numberDiff line numberDiff line change
@@ -1254,6 +1254,62 @@ public void testNoCrashOnGetprops() throws Exception {
12541254
"}"));
12551255
}
12561256

1257+
@Test
1258+
public void testReferenceNonLegacyGoogModule_inScript_required_warns() {
1259+
test(
1260+
srcs(
1261+
"goog.module('test.a'); exports.x = 1;",
1262+
"goog.provide('test.b'); goog.require('test.a'); console.log(test.a.x);"),
1263+
error(CheckMissingRequires.NON_LEGACY_GOOG_MODULE_REFERENCE));
1264+
}
1265+
1266+
@Test
1267+
public void testReferenceNonLegacyGoogModule_inScript_typePosition_required_noWarning() {
1268+
checkNoWarning(
1269+
"""
1270+
goog.module('test.a');
1271+
/** @interface */
1272+
exports.C = class {};\
1273+
""",
1274+
"""
1275+
goog.provide('test.b');
1276+
goog.require('test.a');
1277+
/** @implements {test.a.C} */
1278+
test.b.C = class {
1279+
/** @param {!test.a.C} c */
1280+
foo(c) {}
1281+
};
1282+
""");
1283+
}
1284+
1285+
@Test
1286+
public void testReferenceNonLegacyGoogModule_inScript_typePosition_and_code_required_warning() {
1287+
test(
1288+
srcs(
1289+
"""
1290+
goog.module('test.a');
1291+
/** @interface */
1292+
exports.C = class {};\
1293+
""",
1294+
"""
1295+
goog.provide('test.b');
1296+
goog.require('test.a');
1297+
/** @implements {test.a.C} */
1298+
test.b.C = class {
1299+
/** @param {!test.a.C} c */
1300+
foo(c) { test.a.C; } // reports error
1301+
};
1302+
"""),
1303+
error(CheckMissingRequires.NON_LEGACY_GOOG_MODULE_REFERENCE));
1304+
}
1305+
1306+
@Test
1307+
public void tesNonLegacyGoogModule_inScript_required_noWarning() {
1308+
checkNoWarning(
1309+
"goog.module('test.a'); goog.module.declareLegacyNamespace(); exports.x = 1;",
1310+
"goog.provide('test.b'); goog.require('test.a'); console.log(test.a.x);");
1311+
}
1312+
12571313
private void checkNoWarning(String... js) {
12581314
testSame(srcs(js));
12591315
}

0 commit comments

Comments
 (0)