Skip to content

Commit be54db6

Browse files
12wrigjacopybara-github
authored andcommitted
Allow @nosideeffects in function jsdoc outside of externs, and use it as a signal in PureFunctionIdentifier to treat the function as pure.
PiperOrigin-RevId: 508396386
1 parent 4b14496 commit be54db6

File tree

4 files changed

+88
-25
lines changed

4 files changed

+88
-25
lines changed

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -699,11 +699,9 @@ private void validateDefaultValue(Node n) {
699699
}
700700
}
701701

702-
/**
703-
* Check that @nosideeeffects annotations are only present in externs.
704-
*/
702+
/** Check that @modifies annotations are only present in externs. */
705703
private void validateNoSideEffects(Node n, JSDocInfo info) {
706-
// Cannot have @modifies or @nosideeffects in regular (non externs) js. Report errors.
704+
// Cannot have @modifies in regular (non externs) js. Report errors.
707705
if (info == null) {
708706
return;
709707
}
@@ -715,9 +713,6 @@ private void validateNoSideEffects(Node n, JSDocInfo info) {
715713
if (info.hasSideEffectsArgumentsAnnotation() || info.modifiesThis()) {
716714
report(n, INVALID_MODIFIES_ANNOTATION);
717715
}
718-
if (info.isNoSideEffects()) {
719-
report(n, INVALID_NO_SIDE_EFFECT_ANNOTATION);
720-
}
721716
}
722717

723718
/**

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -658,10 +658,27 @@ public void visit(NodeTraversal traversal, Node node, Node parent) {
658658
allFunctionCalls.add(node);
659659
}
660660

661+
if (node.isFunction()) {
662+
JSDocInfo jsdoc = NodeUtil.getBestJSDocInfo(node);
663+
if (jsdoc != null && jsdoc.isNoSideEffects()) {
664+
// Treat all names (both local aliases and exported names) as if they don't have side
665+
// effects.
666+
for (AmbiguatedFunctionSummary summary : summariesForAllNamesOfFunctionByNode.get(node)) {
667+
summary.setIsArtificiallyPure(true);
668+
summary.bitmask = 0; // no side effects
669+
}
670+
}
671+
}
672+
661673
Node root = this.functionScopeStack.getLast().root;
662674
if (root != null) {
663-
// We only need to look at nodes in function scopes.
664675
for (AmbiguatedFunctionSummary summary : summariesForAllNamesOfFunctionByNode.get(root)) {
676+
if (summary.isArtificiallyPure()) {
677+
// Ignore node side effects for summaries that have been marked as artificially pure.
678+
// We can't skip traversing the nodes in case an artificially pure node contains other
679+
// function definitions.
680+
continue;
681+
}
665682
updateSideEffectsForNode(checkNotNull(summary), traversal, node);
666683
}
667684
}
@@ -1197,6 +1214,12 @@ private static boolean calleeAndCallerShareThis(Node invocation) {
11971214
* @return Returns true if the propagation changed the side effects on the caller.
11981215
*/
11991216
boolean propagate(AmbiguatedFunctionSummary callee, AmbiguatedFunctionSummary caller) {
1217+
if (callee.isArtificiallyPure() || caller.isArtificiallyPure()) {
1218+
// Pure callees should never propagate their side effects to their callers
1219+
// - This would already happen - this condition is just a shortcut.
1220+
// Pure callers should never receive side effects propagated from their callees
1221+
return false;
1222+
}
12001223
int initialCallerFlags = caller.bitmask;
12011224

12021225
if (callerIsAlias) {
@@ -1256,6 +1279,7 @@ private static final class AmbiguatedFunctionSummary {
12561279
// The side effect flags for this set of functions.
12571280
// TODO(nickreid): Replace this with a `Node.SideEffectFlags`.
12581281
private int bitmask = 0;
1282+
private boolean isArtificiallyPure = false;
12591283

12601284
/** Adds a new summary node to {@code graph}, storing the node and returning the summary. */
12611285
static AmbiguatedFunctionSummary createInGraph(
@@ -1271,6 +1295,7 @@ private AmbiguatedFunctionSummary(
12711295

12721296
@CanIgnoreReturnValue
12731297
private AmbiguatedFunctionSummary setMask(int mask) {
1298+
checkState(!isArtificiallyPure, "Artificially pure summaries should not be modified");
12741299
bitmask |= mask;
12751300
return this;
12761301
}
@@ -1325,6 +1350,14 @@ boolean hasNoFlagsSet() {
13251350
return this.bitmask == 0;
13261351
}
13271352

1353+
void setIsArtificiallyPure(boolean isArtificiallyPure) {
1354+
this.isArtificiallyPure = isArtificiallyPure;
1355+
}
1356+
1357+
boolean isArtificiallyPure() {
1358+
return isArtificiallyPure;
1359+
}
1360+
13281361
@Override
13291362
@DoNotCall // For debugging only.
13301363
public String toString() {

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

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import static com.google.javascript.jscomp.CheckJSDoc.DISALLOWED_MEMBER_JSDOC;
2222
import static com.google.javascript.jscomp.CheckJSDoc.INVALID_DEFINE_ON_LET;
2323
import static com.google.javascript.jscomp.CheckJSDoc.INVALID_MODIFIES_ANNOTATION;
24-
import static com.google.javascript.jscomp.CheckJSDoc.INVALID_NO_SIDE_EFFECT_ANNOTATION;
2524
import static com.google.javascript.jscomp.CheckJSDoc.JSDOC_IN_BLOCK_COMMENT;
2625
import static com.google.javascript.jscomp.CheckJSDoc.JSDOC_ON_RETURN;
2726
import static com.google.javascript.jscomp.CheckJSDoc.MISPLACED_ANNOTATION;
@@ -957,37 +956,33 @@ public void testBadTypedef_withES6Modules() {
957956
}
958957

959958
@Test
960-
public void testInvalidAnnotation1() {
961-
testError("/** @nosideeffects */ function foo() {}", INVALID_NO_SIDE_EFFECT_ANNOTATION);
959+
public void testNoSideEffectsAnnotation1() {
960+
testSame("/** @nosideeffects */ function foo() {}");
962961
}
963962

964963
@Test
965-
public void testInvalidAnnotation2() {
966-
testError("var f = /** @nosideeffects */ function() {}", INVALID_NO_SIDE_EFFECT_ANNOTATION);
964+
public void testNoSideEffectsAnnotation2() {
965+
testSame("var f = /** @nosideeffects */ function() {}");
967966
}
968967

969968
@Test
970-
public void testInvalidAnnotation3() {
971-
testError("/** @nosideeffects */ var f = function() {}", INVALID_NO_SIDE_EFFECT_ANNOTATION);
969+
public void testNoSideEffectsAnnotation3() {
970+
testSame("/** @nosideeffects */ var f = function() {}");
972971
}
973972

974973
@Test
975-
public void testInvalidAnnotation4() {
976-
testError(
977-
"var f = function() {};" + "/** @nosideeffects */ f.x = function() {}",
978-
INVALID_NO_SIDE_EFFECT_ANNOTATION);
974+
public void testNoSideEffectsAnnotation4() {
975+
testSame("var f = function() {};" + "/** @nosideeffects */ f.x = function() {}");
979976
}
980977

981978
@Test
982-
public void testInvalidAnnotation5() {
983-
testError(
984-
"var f = function() {};" + "f.x = /** @nosideeffects */ function() {}",
985-
INVALID_NO_SIDE_EFFECT_ANNOTATION);
979+
public void testNoSideEffectsAnnotation5() {
980+
testSame("var f = function() {};" + "f.x = /** @nosideeffects */ function() {}");
986981
}
987982

988983
@Test
989-
public void testInvalidAnnotation_withES6Modules() {
990-
testError("export /** @nosideeffects */ function foo() {}", INVALID_NO_SIDE_EFFECT_ANNOTATION);
984+
public void testNoSideEffectsAnnotations() {
985+
testSame("export /** @nosideeffects */ function foo() {}");
991986
}
992987

993988
@Test

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,46 @@ public void testSharedFunctionName2() {
547547
}
548548
}
549549

550+
@Test
551+
public void testMultipleFunctionDefinitionsOnlyOneWithNoSideEffectsJsDoc() {
552+
String source =
553+
lines(
554+
"let b = undefined;",
555+
"if (true) {",
556+
" b = /** @nosideeffects */(function(){throw 0;});",
557+
"} else {",
558+
" b = () => {}",
559+
"}",
560+
"b()");
561+
assertPureCallsMarked(source, ImmutableList.of("b"));
562+
}
563+
564+
@Test
565+
public void testNoSideEffectsNoPropagationFromOtherCalls() {
566+
String source =
567+
lines(
568+
"var hasSideEffects = function() { throw 0; }",
569+
"var foo = /** @nosideeffects */ function(){ hasSideEffects(); return 0; };",
570+
"var bar = function(){ hasSideEffects(); return 0; };",
571+
"foo();",
572+
"bar();");
573+
// bar is not marked as pure because it inherits side effects from hasSideEffects
574+
assertPureCallsMarked(source, ImmutableList.of("foo"));
575+
}
576+
577+
@Test
578+
public void testNoSideEffectsNoPropagationToOtherCalls() {
579+
String source =
580+
lines(
581+
"var liesAboutSideEffects = /** @nosideeffects */ function() { throw 0; }",
582+
"var foo = function(){ liesAboutSideEffects(); return 0; };",
583+
"var bar = function(){ liesAboutSideEffects(); return 0; };",
584+
"foo();",
585+
"bar();");
586+
assertPureCallsMarked(
587+
source, ImmutableList.of("liesAboutSideEffects", "liesAboutSideEffects", "foo", "bar"));
588+
}
589+
550590
@Test
551591
public void testAnnotationInExternStubs1() {
552592
// In the case where a property is defined first as a stub and then with a FUNCTION:

0 commit comments

Comments
 (0)