Skip to content

Commit 3c7f64b

Browse files
12wrigjacopybara-github
authored andcommitted
Improve parsing of closure-unaware code to only suppress parse errors inside the relevant source ranges, instead of any parse error within the scripts annotated with @closureUnaware.
PiperOrigin-RevId: 704755116
1 parent 6f54038 commit 3c7f64b

File tree

4 files changed

+259
-7
lines changed

4 files changed

+259
-7
lines changed

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

+13
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,19 @@
3333
*
3434
* <p>ManageClosureUnawareCode.unwrap() should be run after all other passes have run, to unwrap the
3535
* code and re-expose it to the code-printing stage of the compiler.
36+
*
37+
* <p>Valid "closure-unaware" scripts should:
38+
* <li>be explicitly annotated with a @fileoverview JSDoc comment that also has the @closureUnaware
39+
* JSDoc tag. This is done so that checking whether an arbitrary node is closure-unaware is a
40+
* very quick operation (as the JSDoc from the SCRIPT node determines whether a bit is set on
41+
* the relevant SourceFile object for those AST nodes), and this higher-level check is used in
42+
* several places in the compiler to avoid reporting various errors.
43+
* <li>annotate each expression within the script that is really "closure-unaware" with a JSDoc
44+
* comment with the @closureUnaware JSDoc tag. Currently, these comments must be attached to
45+
* FUNCTION nodes, and the AST inside the BLOCK child node is considered the closure-unaware
46+
* code. This allows the compiler to differentiate between the parts of the AST containing code
47+
* that has to be "closure-aware" (such as closure module system constructs, assertions that
48+
* should optimize away, etc) from the code that is actually closure-unaware.
3649
*/
3750
final class ManageClosureUnawareCode implements CompilerPass {
3851

src/com/google/javascript/jscomp/parsing/IRFactory.java

+53-6
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
import com.google.javascript.rhino.dtoa.DToA;
136136
import java.math.BigInteger;
137137
import java.util.ArrayDeque;
138+
import java.util.ArrayList;
138139
import java.util.Deque;
139140
import java.util.LinkedHashSet;
140141
import java.util.List;
@@ -244,7 +245,7 @@ class IRFactory {
244245

245246
private FeatureSet features = FeatureSet.BARE_MINIMUM;
246247
private Node resultNode;
247-
private boolean isClosureUnawareCode = false;
248+
private final ArrayList<SourceRange> closureUnawareCodeRanges = new ArrayList<>();
248249

249250
private IRFactory(
250251
StaticSourceFile sourceFile,
@@ -647,9 +648,6 @@ private boolean handlePossibleFileOverviewJsDoc(JsDocInfoParser jsDocParser) {
647648
}
648649

649650
JSDocInfo newFileoverview = jsDocParser.getFileOverviewJSDocInfo();
650-
if (newFileoverview != null && newFileoverview.isClosureUnawareCode()) {
651-
this.isClosureUnawareCode = true;
652-
}
653651
if (identical(newFileoverview, this.firstFileoverview)) {
654652
return false;
655653
}
@@ -868,6 +866,10 @@ Node transform(ParseTree tree) {
868866
JSDocInfo info = parseJSDocInfoOnTree(tree);
869867
NonJSDocComment comment = parseNonJSDocCommentAt(tree.getStart(), false);
870868

869+
if (info != null && info.isClosureUnawareCode()) {
870+
this.closureUnawareCodeRanges.add(tree.location);
871+
}
872+
871873
Node node = transformDispatcher.process(tree);
872874

873875
if (info != null) {
@@ -881,6 +883,49 @@ Node transform(ParseTree tree) {
881883
return node;
882884
}
883885

886+
private boolean withinClosureUnawareCodeRange(int line, int lineColumnNo) {
887+
// TODO: b/331840742: Improve the performance of this check, as naive O(n) is not ideal when
888+
// there are a small number of ranges but many checks against those ranges.
889+
for (SourceRange range : this.closureUnawareCodeRanges) {
890+
if (withinRange(range, line, lineColumnNo)) {
891+
return true;
892+
}
893+
}
894+
return false;
895+
}
896+
897+
private final boolean withinRange(SourceRange range, int line, int lineColumnNo) {
898+
int startLine = range.start.line;
899+
int startColumnNo = range.start.column;
900+
int endLine = range.end.line;
901+
int endColumnNo = range.end.column;
902+
// range starts after line
903+
if (afterPosition(startLine, startColumnNo, line, lineColumnNo, true)) {
904+
return false;
905+
}
906+
907+
// range ends before line
908+
if (afterPosition(line, lineColumnNo, endLine, endColumnNo, false)) {
909+
return false;
910+
}
911+
912+
return true;
913+
}
914+
915+
private boolean afterPosition(
916+
int aLine, int aLineColumnNo, int bLine, int bColumnNo, boolean inclusive) {
917+
if (aLine > bLine) {
918+
return true;
919+
}
920+
if (aLine < bLine) {
921+
return false;
922+
}
923+
if (inclusive && aLineColumnNo == bColumnNo) {
924+
return true;
925+
}
926+
return aLineColumnNo > bColumnNo;
927+
}
928+
884929
private Node maybeInjectCastNode(ParseTree node, JSDocInfo info, Node irNode) {
885930
if (node.type == ParseTreeType.PAREN_EXPRESSION && info.hasType()) {
886931
irNode = newNode(Token.CAST, irNode);
@@ -1084,15 +1129,17 @@ private ClosureUnawareCodeSkippingJsDocInfoErroReporter(
10841129

10851130
@Override
10861131
public void error(String message, String sourceName, int line, int lineOffset) {
1087-
if (host.isClosureUnawareCode) {
1132+
// Line numbers are 1-indexed, but the SourcePosition uses 0-indexed.
1133+
if (host.withinClosureUnawareCodeRange(line - 1, lineOffset)) {
10881134
return;
10891135
}
10901136
delegate.error(message, sourceName, line, lineOffset);
10911137
}
10921138

10931139
@Override
10941140
public void warning(String message, String sourceName, int line, int lineOffset) {
1095-
if (host.isClosureUnawareCode) {
1141+
// Line numbers are 1-indexed, but the SourcePosition uses 0-indexed.
1142+
if (host.withinClosureUnawareCodeRange(line - 1, lineOffset)) {
10961143
return;
10971144
}
10981145
delegate.warning(message, sourceName, line, lineOffset);

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

+25
Original file line numberDiff line numberDiff line change
@@ -394,4 +394,29 @@ public void testAllowsSpecifyingAnnotationOnIIFE() {
394394
"goog.module('foo.bar.baz_raw');",
395395
"$jscomp_wrap_closure_unaware_code('{window[\"foo\"]=5}')"));
396396
}
397+
398+
@Test
399+
public void testReparseWithInvalidJSDocTags() {
400+
// @dependency is not a valid JSDoc tag, and for normal code we would issue a parser error.
401+
// However, for closure-unaware code we don't care at all what is in jsdoc comments.
402+
doTest(
403+
lines(
404+
"/**",
405+
" * @fileoverview",
406+
" * @closureUnaware",
407+
" */",
408+
"goog.module('foo.bar.baz_raw');",
409+
"/** @closureUnaware */",
410+
"(function() {",
411+
" /** @dependency */",
412+
" window['foo'] = 5;",
413+
"}).call(globalThis);"),
414+
lines(
415+
"/**",
416+
" * @fileoverview",
417+
" * @closureUnaware",
418+
" */",
419+
"goog.module('foo.bar.baz_raw');",
420+
"$jscomp_wrap_closure_unaware_code('{window[\"foo\"]=5}')"));
421+
}
397422
}

test/com/google/javascript/jscomp/integration/ClosureUnawareCodeIntegrationTest.java

+168-1
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,6 @@ public void testNoOptimizeClosureUnawareCode_parsesCodeWithNonClosureJsDoc() {
470470
CompilerOptions options = createCompilerOptions();
471471
CompilationLevel.ADVANCED_OPTIMIZATIONS.setOptionsForCompilationLevel(options);
472472
CompilationLevel.ADVANCED_OPTIMIZATIONS.setTypeBasedOptimizationOptions(options);
473-
options.setLanguageOut(LanguageMode.ECMASCRIPT_2018);
474473

475474
externs =
476475
ImmutableList.<SourceFile>builder()
@@ -492,12 +491,180 @@ public void testNoOptimizeClosureUnawareCode_parsesCodeWithNonClosureJsDoc() {
492491
"(function() {",
493492
" /**",
494493
" * @prop {number} a - scale x",
494+
// @defaults isn't a valid jsdoc tag, but within the closure-unaware portions of the
495+
// AST we should not raise a warning.
496+
" * @defaults",
497+
" */",
498+
" const x = 5;",
499+
"}).call(globalThis);"),
500+
lines("(function() {", " const x = 5;", "}).call(globalThis);"));
501+
}
502+
503+
@Test
504+
public void
505+
testNoOptimizeClosureUnawareCode_parsesCodeWithNonClosureJsDoc_whenNotAttachedToNode() {
506+
CompilerOptions options = createCompilerOptions();
507+
CompilationLevel.ADVANCED_OPTIMIZATIONS.setOptionsForCompilationLevel(options);
508+
CompilationLevel.ADVANCED_OPTIMIZATIONS.setTypeBasedOptimizationOptions(options);
509+
510+
externs =
511+
ImmutableList.<SourceFile>builder()
512+
.addAll(externs)
513+
.add(
514+
SourceFile.fromCode(
515+
"globalthis.js", lines("/**", " * @type {?}", " */", "var globalThis;")))
516+
.build();
517+
518+
// this test-case is trying to ensure that even when there are jsdoc comments in the
519+
// closure-unaware portions of the AST that aren't attached to specific nodes / expressions
520+
// that we can still ignore invalid jsdoc contents.
521+
// @defaults isn't a valid jsdoc tag, and outside of the closure-unaware portions of the AST we
522+
// should raise a warning.
523+
524+
test(
525+
options,
526+
lines(
527+
"/**",
528+
" * @fileoverview",
529+
" * @closureUnaware",
530+
" */",
531+
"goog.module('a.b');",
532+
"/** @closureUnaware */",
533+
"(function() {",
534+
" /** @defaults */", // JSDoc comment not attached to any node
535+
" /**",
536+
" * @prop {number} a - scale x",
537+
" * @defaults",
495538
" */",
496539
" const x = 5;",
497540
"}).call(globalThis);"),
498541
lines("(function() {", " const x = 5;", "}).call(globalThis);"));
499542
}
500543

544+
@Test
545+
public void
546+
testNoOptimizeClosureUnawareCode_parsesCodeWithNonClosureJsDoc_whenNotAttachedToNode_typedJsDoc() {
547+
CompilerOptions options = createCompilerOptions();
548+
CompilationLevel.ADVANCED_OPTIMIZATIONS.setOptionsForCompilationLevel(options);
549+
CompilationLevel.ADVANCED_OPTIMIZATIONS.setTypeBasedOptimizationOptions(options);
550+
551+
externs =
552+
ImmutableList.<SourceFile>builder()
553+
.addAll(externs)
554+
.add(
555+
SourceFile.fromCode(
556+
"globalthis.js", lines("/**", " * @type {?}", " */", "var globalThis;")))
557+
.build();
558+
559+
// this test-case is trying to ensure that even when there are jsdoc comments in the
560+
// closure-unaware portions of the AST that aren't attached to specific nodes / expressions
561+
// that we can still ignore invalid jsdoc contents.
562+
// @type [string] does not parse as a valid "typed jsdoc" tag, and this follows a different
563+
// parsing codepath than non-type jsdoc tags and raises a different error
564+
// than an entirely unknown jsdoc tag.
565+
566+
test(
567+
options,
568+
lines(
569+
"/**",
570+
" * @fileoverview",
571+
" * @closureUnaware",
572+
" */",
573+
"goog.module('a.b');",
574+
"/** @closureUnaware */",
575+
"(function() {",
576+
" /** @type [string] */",
577+
"",
578+
" /**",
579+
" * @prop {number} a - scale x",
580+
" * @defaults",
581+
" */",
582+
" const x = 5;",
583+
"}).call(globalThis);"),
584+
lines("(function() {", " const x = 5;", "}).call(globalThis);"));
585+
}
586+
587+
@Test
588+
public void
589+
testNoOptimizeClosureUnawareCode_doesntSuppressParseErrorsOutsideClosureUnawareBlocks() {
590+
CompilerOptions options = createCompilerOptions();
591+
CompilationLevel.ADVANCED_OPTIMIZATIONS.setOptionsForCompilationLevel(options);
592+
CompilationLevel.ADVANCED_OPTIMIZATIONS.setTypeBasedOptimizationOptions(options);
593+
594+
externs =
595+
ImmutableList.<SourceFile>builder()
596+
.addAll(externs)
597+
.add(
598+
SourceFile.fromCode(
599+
"globalthis.js", lines("/**", " * @type {?}", " */", "var globalThis;")))
600+
.build();
601+
602+
// this test-case is trying to ensure that even when the special handling for JSDoc comments in
603+
// closureUnaware code we can still report jsdoc errors for code that is not within the
604+
// closure-unaware subsection of the AST.
605+
test(
606+
options,
607+
lines(
608+
"/**",
609+
" * @fileoverview",
610+
" * @closureUnaware",
611+
" */",
612+
"goog.module('a.b');",
613+
// This is invalid JSDoc, but it isn't within the subtree of a node annotated as
614+
// `@closureUnaware`.
615+
// NOTE: The `@closureUnaware` in the `@fileoverview` comment does not apply this
616+
// subtree suppression
617+
// mechanism to the whole script - it is only to indicate that the file contains some
618+
// closure-unaware code
619+
// (e.g. a performance optimization).
620+
"/**",
621+
" * @prop {number} a - scale x",
622+
" */",
623+
"/** @closureUnaware */",
624+
"(function() {",
625+
" const x = 5;",
626+
"}).call(globalThis);"),
627+
DiagnosticGroups.NON_STANDARD_JSDOC);
628+
}
629+
630+
@Test
631+
public void
632+
testNoOptimizeClosureUnawareCode_doesntSuppressParseErrorsOutsideClosureUnawareBlocks_afterRange() {
633+
CompilerOptions options = createCompilerOptions();
634+
CompilationLevel.ADVANCED_OPTIMIZATIONS.setOptionsForCompilationLevel(options);
635+
CompilationLevel.ADVANCED_OPTIMIZATIONS.setTypeBasedOptimizationOptions(options);
636+
637+
externs =
638+
ImmutableList.<SourceFile>builder()
639+
.addAll(externs)
640+
.add(
641+
SourceFile.fromCode(
642+
"globalthis.js", lines("/**", " * @type {?}", " */", "var globalThis;")))
643+
.build();
644+
645+
// this test-case is trying to ensure that even when the special handling for JSDoc comments in
646+
// closureUnaware code we can still report jsdoc errors for code that is not within the
647+
// closure-unaware subsection of the AST.
648+
// Specifically this test validates that any comments that come after a closure-unaware
649+
// subsection of the AST don't have their jsdoc parse errors suppressed.
650+
test(
651+
options,
652+
lines(
653+
"/**",
654+
" * @fileoverview",
655+
" * @closureUnaware",
656+
" */",
657+
"goog.module('a.b');",
658+
"/** @closureUnaware */",
659+
"(function() {",
660+
" const x = 5;",
661+
"}).call(globalThis);",
662+
"/**",
663+
" * @prop {number} a - scale x",
664+
" */",
665+
""),
666+
DiagnosticGroups.NON_STANDARD_JSDOC);
667+
}
501668
// TODO how can I test whether source info is being properly retained?
502669
// TODO if there is a sourcemap comment in the IIFE, can we use that info somehow?
503670
}

0 commit comments

Comments
 (0)