32
32
import com .google .javascript .rhino .Node ;
33
33
import com .google .javascript .rhino .QualifiedName ;
34
34
import java .util .HashMap ;
35
+ import java .util .HashSet ;
35
36
import java .util .Map ;
37
+ import java .util .Set ;
36
38
import java .util .regex .Pattern ;
37
39
import org .jspecify .nullness .Nullable ;
38
40
@@ -81,10 +83,8 @@ public final class GatherModuleMetadata implements CompilerPass {
81
83
static final DiagnosticType INVALID_NESTED_LOAD_MODULE =
82
84
DiagnosticType .error ("JSC_INVALID_NESTED_LOAD_MODULE" , "goog.loadModule cannot be nested." );
83
85
84
- static final DiagnosticType INVALID_READ_TOGGLE =
85
- DiagnosticType .error (
86
- "JSC_INVALID_READ_TOGGLE" ,
87
- "Argument to goog.readToggleInternalDoNotCallDirectly must be a string." );
86
+ static final DiagnosticType INVALID_TOGGLE_USAGE =
87
+ DiagnosticType .error ("JSC_INVALID_TOGGLE_USAGE" , "Invalid toggle usage: {0}" );
88
88
89
89
private static final Node GOOG_PROVIDE = IR .getprop (IR .name ("goog" ), "provide" );
90
90
private static final Node GOOG_MODULE = IR .getprop (IR .name ("goog" ), "module" );
@@ -95,13 +95,13 @@ public final class GatherModuleMetadata implements CompilerPass {
95
95
private static final Node GOOG_MODULE_DECLARELEGACYNAMESPACE =
96
96
IR .getprop (GOOG_MODULE .cloneTree (), "declareLegacyNamespace" );
97
97
private static final Node GOOG_DECLARE_MODULE_ID = IR .getprop (IR .name ("goog" ), "declareModuleId" );
98
- private static final Node GOOG_READ_TOGGLE =
99
- IR .getprop (IR .name ("goog" ), "readToggleInternalDoNotCallDirectly" );
100
98
101
99
// TODO(johnplaisted): Remove once clients have migrated to declareModuleId
102
100
private static final Node GOOG_MODULE_DECLARNAMESPACE =
103
101
IR .getprop (GOOG_MODULE .cloneTree (), "declareNamespace" );
104
102
103
+ private static final String TOGGLE_NAME_PREFIX = "TOGGLE_" ;
104
+
105
105
/**
106
106
* Map from module path to module. These modules represent files and thus will contain all goog
107
107
* namespaces that are in the file. These are not the same modules in modulesByGoogNamespace.
@@ -216,6 +216,14 @@ ModuleMetadata build() {
216
216
217
217
/** Traverses the AST and build a sets of {@link ModuleMetadata}s. */
218
218
private final class Finder implements NodeTraversal .Callback {
219
+
220
+ // Store both names and vars. Strings alone is insufficient to determine whether a name is
221
+ // actually a toggle module (since it could have been shadowed, or may have been defined in a
222
+ // different file), but looking up by only vars is much slower. This way we can do a fast name
223
+ // lookup, followed by a slower var lookup only if the name is known to be a toggle module name.
224
+ final Set <String > toggleModuleNames = new HashSet <>();
225
+ final Set <Var > toggleModules = new HashSet <>();
226
+
219
227
@ Override
220
228
public boolean shouldTraverse (NodeTraversal t , Node n , Node parent ) {
221
229
switch (n .getToken ()) {
@@ -337,7 +345,22 @@ private boolean isFromGoogImport(Var goog) {
337
345
}
338
346
339
347
private void visitName (NodeTraversal t , Node n ) {
340
- if (!"goog" .equals (n .getString ())) {
348
+ String name = n .getString ();
349
+ if (toggleModuleNames .contains (name )) {
350
+ Var nameVar = t .getScope ().getVar (name );
351
+ if (toggleModules .contains (nameVar )) {
352
+ Node parent = n .getParent ();
353
+ if (parent .isGetProp ()) {
354
+ addToggle (t , n , parent .getString ());
355
+ } else if (!NodeUtil .isNameDeclaration (parent )) {
356
+ t .report (
357
+ n ,
358
+ INVALID_TOGGLE_USAGE ,
359
+ "toggle modules may not be used other than looking up properties" );
360
+ }
361
+ }
362
+ }
363
+ if (!"goog" .equals (name )) {
341
364
return ;
342
365
}
343
366
@@ -421,10 +444,31 @@ private void visitGoogCall(NodeTraversal t, Node n) {
421
444
}
422
445
} else if (getprop .matchesQualifiedName (GOOG_REQUIRE )) {
423
446
if (n .hasTwoChildren () && n .getLastChild ().isStringLit ()) {
424
- currentModule
425
- .metadataBuilder
426
- .stronglyRequiredGoogNamespacesBuilder ()
427
- .add (n .getLastChild ().getString ());
447
+ String namespace = n .getLastChild ().getString ();
448
+ currentModule .metadataBuilder .stronglyRequiredGoogNamespacesBuilder ().add (namespace );
449
+ if (namespace .endsWith ("$2etoggles" )) {
450
+ // Track imports of *.toggles.ts, which are rewritten to $2etoggles.
451
+ Node callParent = n .getParent ();
452
+ Node lhs = callParent .getFirstChild ();
453
+ if (callParent .isDestructuringLhs ()) {
454
+ // const {TOGGLE_foo} = goog.require('foo$2etoggles');
455
+ for (Node key : lhs .children ()) {
456
+ if (key .isStringKey ()) {
457
+ addToggle (t , n , key .getString ());
458
+ } else {
459
+ t .report (n , INVALID_TOGGLE_USAGE , "must be destructured with string keys" );
460
+ }
461
+ }
462
+ } else if (callParent .isName ()) {
463
+ // const fooToggles = goog.require('foo$2etoggles');
464
+ String name = callParent .getString ();
465
+ Var nameVar = t .getScope ().getVar (name );
466
+ toggleModules .add (nameVar );
467
+ toggleModuleNames .add (name );
468
+ } else {
469
+ t .report (n , INVALID_TOGGLE_USAGE , "import must be assigned" );
470
+ }
471
+ }
428
472
} else {
429
473
t .report (n , INVALID_REQUIRE_NAMESPACE );
430
474
}
@@ -452,12 +496,16 @@ private void visitGoogCall(NodeTraversal t, Node n) {
452
496
} else {
453
497
t .report (n , INVALID_REQUIRE_DYNAMIC );
454
498
}
455
- } else if (getprop .matchesQualifiedName (GOOG_READ_TOGGLE )) {
456
- if (n .hasTwoChildren () && n .getLastChild ().isStringLit ()) {
457
- currentModule .metadataBuilder .readTogglesBuilder ().add (n .getLastChild ().getString ());
458
- } else {
459
- t .report (n , INVALID_READ_TOGGLE );
460
- }
499
+ }
500
+ }
501
+
502
+ /** Record a toggle usage (either a destructured import or a property lookup on a module). */
503
+ private void addToggle (NodeTraversal t , Node n , String name ) {
504
+ if (name .startsWith (TOGGLE_NAME_PREFIX )) {
505
+ String toggleName = name .substring (TOGGLE_NAME_PREFIX .length ());
506
+ currentModule .metadataBuilder .readTogglesBuilder ().add (toggleName );
507
+ } else {
508
+ t .report (n , INVALID_TOGGLE_USAGE , "all toggle names must start with `TOGGLE_`" );
461
509
}
462
510
}
463
511
0 commit comments