aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/tools/skylark
diff options
context:
space:
mode:
authorGravatar brandjon <brandjon@google.com>2017-08-30 18:33:54 +0200
committerGravatar Vladimir Moskva <vladmos@google.com>2017-08-31 13:32:58 +0200
commitf12402b2627f8eeb70ee24f457efe007206f9b42 (patch)
tree4ac66f42135291fddb6dec449b8f33eca1eb3cd8 /src/tools/skylark
parent4a84bcc629c83c588e3d50b176f71357fd74b73f (diff)
Make ControlFlowChecker follow the dataflow analysis idiom more closely
This makes ControlFlowInfo an ADT handling its own join operation, and combines the list of return statements into that structure. This is less error-prone if a new field is added, and helps simplify the logic in visit(IfStatement). The invariants regarding the cf field are also clarified: It is "transferred" (in dataflow analysis terminology) from the point before the node to the point after the node by the visit() function. It is also undefined (null) outside of a function definition. There's a cost to this CL in terms of LOC and maybe even memory allocations, but I suspect that's outweighed by sticking to a known paradigm for flow analysis. RELNOTES: None PiperOrigin-RevId: 167005045
Diffstat (limited to 'src/tools/skylark')
-rw-r--r--src/tools/skylark/java/com/google/devtools/skylark/skylint/ControlFlowChecker.java136
-rw-r--r--src/tools/skylark/java/com/google/devtools/skylark/skylint/Skylint.java8
-rw-r--r--src/tools/skylark/javatests/com/google/devtools/skylark/skylint/ControlFlowCheckerTest.java7
3 files changed, 120 insertions, 31 deletions
diff --git a/src/tools/skylark/java/com/google/devtools/skylark/skylint/ControlFlowChecker.java b/src/tools/skylark/java/com/google/devtools/skylark/skylint/ControlFlowChecker.java
index 355069a6bf..e11c3d2a5f 100644
--- a/src/tools/skylark/java/com/google/devtools/skylark/skylint/ControlFlowChecker.java
+++ b/src/tools/skylark/java/com/google/devtools/skylark/skylint/ControlFlowChecker.java
@@ -14,6 +14,7 @@
package com.google.devtools.skylark.skylint;
+import com.google.common.base.Preconditions;
import com.google.devtools.build.lib.syntax.BuildFileAST;
import com.google.devtools.build.lib.syntax.Expression;
import com.google.devtools.build.lib.syntax.ExpressionStatement;
@@ -26,8 +27,10 @@ import com.google.devtools.build.lib.syntax.ReturnStatement;
import com.google.devtools.build.lib.syntax.Statement;
import com.google.devtools.build.lib.syntax.SyntaxTreeVisitor;
import java.util.ArrayList;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.stream.Stream;
+import javax.annotation.Nullable;
/**
* Performs lints related to control flow.
@@ -38,8 +41,19 @@ import java.util.stream.Stream;
// TODO(skylark-team): Check for unreachable statements
public class ControlFlowChecker extends SyntaxTreeVisitor {
private final List<Issue> issues = new ArrayList<>();
- private ControlFlowInfo cf = new ControlFlowInfo();
- private List<ReturnStatement> returnStatementsWithoutValue = new ArrayList<>();
+
+ /**
+ * Represents the analyzed info at the current program point. The {@code visit()} methods
+ * implement the transfer function from the program point immediately before that AST node to the
+ * program point immediately after that node. This destructively consumes (modifies) the CFI
+ * object, so for branching nodes a copy must be made.
+ *
+ * <p>See also: https://en.wikipedia.org/wiki/Data-flow_analysis
+ *
+ * <p>This is always null whenever we're not in a function definition.
+ */
+ @Nullable
+ private ControlFlowInfo cfi = null;
public static List<Issue> check(BuildFileAST ast) {
ControlFlowChecker checker = new ControlFlowChecker();
@@ -49,41 +63,45 @@ public class ControlFlowChecker extends SyntaxTreeVisitor {
@Override
public void visit(IfStatement node) {
- ControlFlowInfo saved = cf;
- boolean someBranchHasReturnWithValue = false;
- boolean someBranchHasReturnWithoutValue = false;
- boolean allBranchesReturnAlwaysExplicitly = true;
+ if (cfi == null) {
+ return;
+ }
+ // Save the input cfi, copy its state to seed each branch, then gather the branches together
+ // with a join operation to produces the output cfi.
+ ControlFlowInfo input = cfi;
+ ArrayList<ControlFlowInfo> outputs = new ArrayList<>();
Stream<List<Statement>> branches =
Stream.concat(
node.getThenBlocks().stream().map(ConditionalStatements::getStatements),
Stream.of(node.getElseBlock()));
for (List<Statement> branch : (Iterable<List<Statement>>) branches::iterator) {
- cf = new ControlFlowInfo();
+ cfi = ControlFlowInfo.copy(input);
visitAll(branch);
- someBranchHasReturnWithValue |= cf.hasReturnWithValue;
- someBranchHasReturnWithoutValue |= cf.hasReturnWithoutValue;
- allBranchesReturnAlwaysExplicitly &= cf.returnsAlwaysExplicitly;
+ outputs.add(cfi);
}
- cf.hasReturnWithValue = saved.hasReturnWithValue || someBranchHasReturnWithValue;
- cf.hasReturnWithoutValue = saved.hasReturnWithoutValue || someBranchHasReturnWithoutValue;
- cf.returnsAlwaysExplicitly = saved.returnsAlwaysExplicitly || allBranchesReturnAlwaysExplicitly;
+ cfi = ControlFlowInfo.join(outputs);
}
@Override
public void visit(ReturnStatement node) {
- cf.returnsAlwaysExplicitly = true;
+ // Should be rejected by parser, but we may have been fed a bad AST.
+ Preconditions.checkState(cfi != null, "AST has illegal top-level return statement");
+ cfi.returnsAlwaysExplicitly = true;
if (node.getReturnExpression() != null) {
- cf.hasReturnWithValue = true;
+ cfi.hasReturnWithValue = true;
} else {
- cf.hasReturnWithoutValue = true;
- returnStatementsWithoutValue.add(node);
+ cfi.hasReturnWithoutValue = true;
+ cfi.returnStatementsWithoutValue.add(new Return(node));
}
}
@Override
public void visit(ExpressionStatement node) {
+ if (cfi == null) {
+ return;
+ }
if (isFail(node.getExpression())) {
- cf.returnsAlwaysExplicitly = true;
+ cfi.returnsAlwaysExplicitly = true;
}
}
@@ -97,26 +115,92 @@ public class ControlFlowChecker extends SyntaxTreeVisitor {
@Override
public void visit(FunctionDefStatement node) {
- cf = new ControlFlowInfo();
- returnStatementsWithoutValue = new ArrayList<>();
+ Preconditions.checkState(cfi == null);
+ cfi = ControlFlowInfo.entry();
super.visit(node);
- if (cf.hasReturnWithValue && (!cf.returnsAlwaysExplicitly || cf.hasReturnWithoutValue)) {
+ if (cfi.hasReturnWithValue && (!cfi.returnsAlwaysExplicitly || cfi.hasReturnWithoutValue)) {
issues.add(
new Issue(
"some but not all execution paths of '" + node.getIdentifier() + "' return a value",
node.getLocation()));
- for (ReturnStatement returnStatement : returnStatementsWithoutValue) {
+ for (Return returnWrapper : cfi.returnStatementsWithoutValue) {
issues.add(
new Issue(
"return value missing (you can `return None` if this is desired)",
- returnStatement.getLocation()));
+ returnWrapper.node.getLocation()));
+ }
+ }
+ cfi = null;
+ }
+
+ /**
+ * Wrapper around {@code ReturnStatement} that supports hashing and equality based on the
+ * identity of the node it wraps.
+ */
+ private static class Return {
+
+ final ReturnStatement node;
+
+ Return(ReturnStatement node) {
+ this.node = node;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof Return)) {
+ return false;
}
+ return this.node == ((Return) other).node;
+ }
+
+ @Override
+ public int hashCode() {
+ return System.identityHashCode(node);
}
}
private static class ControlFlowInfo {
- boolean hasReturnWithValue = false;
- boolean hasReturnWithoutValue = false;
- boolean returnsAlwaysExplicitly = false;
+ private boolean hasReturnWithValue;
+ private boolean hasReturnWithoutValue;
+ private boolean returnsAlwaysExplicitly;
+ private final LinkedHashSet<Return> returnStatementsWithoutValue;
+
+ private ControlFlowInfo(
+ boolean hasReturnWithValue,
+ boolean hasReturnWithoutValue,
+ boolean returnsAlwaysExplicitly,
+ LinkedHashSet<Return> returnStatementsWithoutValue) {
+ this.hasReturnWithValue = hasReturnWithValue;
+ this.hasReturnWithoutValue = hasReturnWithoutValue;
+ this.returnsAlwaysExplicitly = returnsAlwaysExplicitly;
+ this.returnStatementsWithoutValue = returnStatementsWithoutValue;
+ }
+
+ /** Create a CFI corresponding to an entry point in the control-flow graph. */
+ static ControlFlowInfo entry() {
+ return new ControlFlowInfo(false, false, false, new LinkedHashSet<>());
+ }
+
+ /** Creates a copy of a CFI, including the {@code returnStatementsWithoutValue} collection. */
+ static ControlFlowInfo copy(ControlFlowInfo existing) {
+ return new ControlFlowInfo(
+ existing.hasReturnWithValue,
+ existing.hasReturnWithoutValue,
+ existing.returnsAlwaysExplicitly,
+ new LinkedHashSet<>(existing.returnStatementsWithoutValue));
+ }
+
+ /** Joins the CFIs for several alternative paths together. */
+ static ControlFlowInfo join(List<ControlFlowInfo> infos) {
+ ControlFlowInfo result = new ControlFlowInfo(
+ false, false, true, new LinkedHashSet<>());
+ for (ControlFlowInfo info : infos) {
+ result.hasReturnWithValue |= info.hasReturnWithValue;
+ result.hasReturnWithoutValue |= info.hasReturnWithoutValue;
+ result.returnsAlwaysExplicitly &= info.returnsAlwaysExplicitly;
+ result.returnStatementsWithoutValue.addAll(info.returnStatementsWithoutValue);
+ }
+ return result;
+ }
}
}
diff --git a/src/tools/skylark/java/com/google/devtools/skylark/skylint/Skylint.java b/src/tools/skylark/java/com/google/devtools/skylark/skylint/Skylint.java
index b9eb83f73a..92e2f5ae1d 100644
--- a/src/tools/skylark/java/com/google/devtools/skylark/skylint/Skylint.java
+++ b/src/tools/skylark/java/com/google/devtools/skylark/skylint/Skylint.java
@@ -41,11 +41,9 @@ public class Skylint {
issues.sort(Issue::compare);
if (!issues.isEmpty()) {
System.out.println(path);
- }
- for (Issue issue : issues) {
- System.out.println(issue);
- }
- if (!issues.isEmpty()) {
+ for (Issue issue : issues) {
+ System.out.println(issue);
+ }
System.exit(1);
}
}
diff --git a/src/tools/skylark/javatests/com/google/devtools/skylark/skylint/ControlFlowCheckerTest.java b/src/tools/skylark/javatests/com/google/devtools/skylark/skylint/ControlFlowCheckerTest.java
index 7e7addd8a2..0b00745ff2 100644
--- a/src/tools/skylark/javatests/com/google/devtools/skylark/skylint/ControlFlowCheckerTest.java
+++ b/src/tools/skylark/javatests/com/google/devtools/skylark/skylint/ControlFlowCheckerTest.java
@@ -35,6 +35,13 @@ public class ControlFlowCheckerTest {
}
@Test
+ public void testAnalyzerToleratesTopLevelFail() throws Exception {
+ Truth.assertThat(
+ findIssues("fail(\"fail is considered a return, but not at the top level\")"))
+ .isEmpty();
+ }
+
+ @Test
public void testIfElseReturnMissing() throws Exception {
Truth.assertThat(
findIssues(