Starlark AST: use a list of bindings instead of a map in load statements
- The map was not useful: all we want to do is to iterate on the bindings
- Using Identifier as a key is not well defined (we shouldn't compare them and they shouldn't be hashable in the first place)
- Using Identifier instead of String for the original name allows us to preserve the location in the AST
RELNOTES: None.
PiperOrigin-RevId: 220860272
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Eval.java b/src/main/java/com/google/devtools/build/lib/syntax/Eval.java
index d7e30dd..42e798e 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Eval.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Eval.java
@@ -17,7 +17,6 @@
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
-import java.util.Map;
import java.util.function.Function;
/**
@@ -145,10 +144,10 @@
}
void execLoad(LoadStatement node) throws EvalException, InterruptedException {
- for (Map.Entry<Identifier, String> entry : node.getSymbolMap().entrySet()) {
+ for (LoadStatement.Binding binding : node.getBindings()) {
try {
- Identifier name = entry.getKey();
- Identifier declared = Identifier.of(entry.getValue());
+ Identifier name = binding.getLocalName();
+ Identifier declared = binding.getOriginalName();
if (declared.isPrivate()) {
throw new EvalException(
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/LoadStatement.java b/src/main/java/com/google/devtools/build/lib/syntax/LoadStatement.java
index 9134075..cb2a7e7 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/LoadStatement.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/LoadStatement.java
@@ -14,34 +14,53 @@
package com.google.devtools.build.lib.syntax;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
import java.io.IOException;
-import java.util.Map;
+import java.io.Serializable;
+import java.util.List;
/** Syntax node for an import statement. */
public final class LoadStatement extends Statement {
- private final ImmutableMap<Identifier, String> symbolMap;
+ /**
+ * Binding represents a binding in a load statement. load("...", local = "orig")
+ *
+ * <p>If there's no alias, a single Identifier can be used for both local and orig.
+ */
+ public static final class Binding implements Serializable {
+ private final Identifier local;
+ private final Identifier orig;
+
+ public Identifier getLocalName() {
+ return local;
+ }
+
+ public Identifier getOriginalName() {
+ return orig;
+ }
+
+ public Binding(Identifier localName, Identifier originalName) {
+ this.local = localName;
+ this.orig = originalName;
+ }
+ }
+
+ private final ImmutableList<Binding> bindings;
private final StringLiteral imp;
/**
* Constructs an import statement.
*
- * <p>{@code symbols} maps a symbol to the original name under which it was defined in
- * the bzl file that should be loaded. If aliasing is used, the value differs from its key's
- * {@code symbol.getName()}. Otherwise, both values are identical.
+ * <p>{@code symbols} maps a symbol to the original name under which it was defined in the bzl
+ * file that should be loaded. If aliasing is used, the value differs from its key's {@code
+ * symbol.getName()}. Otherwise, both values are identical.
*/
- public LoadStatement(StringLiteral imp, Map<Identifier, String> symbolMap) {
+ public LoadStatement(StringLiteral imp, List<Binding> bindings) {
this.imp = imp;
- this.symbolMap = ImmutableMap.copyOf(symbolMap);
+ this.bindings = ImmutableList.copyOf(bindings);
}
- public ImmutableMap<Identifier, String> getSymbolMap() {
- return symbolMap;
- }
-
- public ImmutableList<Identifier> getSymbols() {
- return symbolMap.keySet().asList();
+ public ImmutableList<Binding> getBindings() {
+ return bindings;
}
public StringLiteral getImport() {
@@ -53,15 +72,16 @@
printIndent(buffer, indentLevel);
buffer.append("load(");
imp.prettyPrint(buffer);
- for (Identifier symbol : symbolMap.keySet()) {
+ for (Binding binding : bindings) {
buffer.append(", ");
- String origName = symbolMap.get(symbol);
- if (origName.equals(symbol.getName())) {
+ Identifier local = binding.getLocalName();
+ String origName = binding.getOriginalName().getName();
+ if (origName.equals(local.getName())) {
buffer.append('"');
- symbol.prettyPrint(buffer);
+ local.prettyPrint(buffer);
buffer.append('"');
} else {
- symbol.prettyPrint(buffer);
+ local.prettyPrint(buffer);
buffer.append("=\"");
buffer.append(origName);
buffer.append('"');
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Parser.java b/src/main/java/com/google/devtools/build/lib/syntax/Parser.java
index 3524e84..dea8f33 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Parser.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Parser.java
@@ -30,9 +30,10 @@
import com.google.devtools.build.lib.syntax.IfStatement.ConditionalStatements;
import java.util.ArrayList;
import java.util.EnumSet;
-import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import javax.annotation.Nullable;
/**
@@ -1039,8 +1040,11 @@
}
expect(TokenKind.COMMA);
- Map<Identifier, String> symbols = new HashMap<>();
- parseLoadSymbol(symbols); // At least one symbol is required
+ ImmutableList.Builder<LoadStatement.Binding> bindings = ImmutableList.builder();
+ // previousSymbols is used to detect duplicate symbols in the same statement.
+ Set<String> previousSymbols = new HashSet<>();
+
+ parseLoadSymbol(bindings, previousSymbols); // At least one symbol is required
while (token.kind != TokenKind.RPAREN && token.kind != TokenKind.EOF) {
expect(TokenKind.COMMA);
@@ -1048,10 +1052,10 @@
break;
}
- parseLoadSymbol(symbols);
+ parseLoadSymbol(bindings, previousSymbols);
}
- LoadStatement stmt = new LoadStatement(importString, symbols);
+ LoadStatement stmt = new LoadStatement(importString, bindings.build());
list.add(setLocation(stmt, start, token.right));
expect(TokenKind.RPAREN);
expectAndRecover(TokenKind.NEWLINE);
@@ -1060,39 +1064,41 @@
/**
* Parses the next symbol argument of a load statement and puts it into the output map.
*
- * <p> The symbol is either "name" (STRING) or name = "declared" (IDENTIFIER EQUALS STRING).
- * If no alias is used, "name" and "declared" will be identical. "Declared" refers to the
- * original name in the Bazel file that should be loaded, while "name" will be the key of the
- * entry in the map.
+ * <p>The symbol is either "name" (STRING) or name = "declared" (IDENTIFIER EQUALS STRING). If no
+ * alias is used, "name" and "declared" will be identical. "Declared" refers to the original name
+ * in the Bazel file that should be loaded, while "name" will be the key of the entry in the map.
*/
- private void parseLoadSymbol(Map<Identifier, String> symbols) {
+ private void parseLoadSymbol(
+ ImmutableList.Builder<LoadStatement.Binding> symbols, Set<String> previousSymbols) {
if (token.kind != TokenKind.STRING && token.kind != TokenKind.IDENTIFIER) {
syntaxError("expected either a literal string or an identifier");
return;
}
String name = (String) token.value;
- Identifier identifier = Identifier.of(name);
- if (symbols.containsKey(identifier)) {
- syntaxError(
- String.format("Identifier '%s' is used more than once", identifier.getName()));
- }
- setLocation(identifier, token.left, token.right);
+ Identifier local = setLocation(Identifier.of(name), token.left, token.right);
- String declared;
+ if (previousSymbols.contains(local.getName())) {
+ syntaxError(String.format("Identifier '%s' is used more than once", local.getName()));
+ }
+ previousSymbols.add(local.getName());
+
+ Identifier original;
if (token.kind == TokenKind.STRING) {
- declared = name;
+ // load(..., "name")
+ original = local;
} else {
+ // load(..., local = "orig")
expect(TokenKind.IDENTIFIER);
expect(TokenKind.EQUALS);
if (token.kind != TokenKind.STRING) {
syntaxError("expected string");
return;
}
- declared = token.value.toString();
+ original = setLocation(Identifier.of((String) token.value), token.left, token.right);
}
nextToken();
- symbols.put(identifier, declared);
+ symbols.add(new LoadStatement.Binding(local, original));
}
private void parseTopLevelStatement(List<Statement> list) {
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SyntaxTreeVisitor.java b/src/main/java/com/google/devtools/build/lib/syntax/SyntaxTreeVisitor.java
index 9845a8e..117bbf0 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SyntaxTreeVisitor.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SyntaxTreeVisitor.java
@@ -92,7 +92,9 @@
}
public void visit(LoadStatement node) {
- visitAll(node.getSymbols());
+ for (LoadStatement.Binding binding : node.getBindings()) {
+ visit(binding.getLocalName());
+ }
}
public void visit(ListLiteral node) {
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java b/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java
index 0ffadaa..e71670d 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java
@@ -134,8 +134,8 @@
declare(fctName.getName(), fctName.getLocation());
break;
case LOAD:
- for (Identifier id : ((LoadStatement) stmt).getSymbols()) {
- declare(id.getName(), id.getLocation());
+ for (LoadStatement.Binding binding : ((LoadStatement) stmt).getBindings()) {
+ declare(binding.getLocalName().getName(), binding.getLocalName().getLocation());
}
break;
case CONDITIONAL: