GN: Throw an error overwriting a nonempty scope.
Previously GN would throw an error if you assigned a nonempty list to another
nonempty list. This is easy to mess up when you actually meant to append.
However, it did not have this protection for "scope"-type values.
Adds such protection to scope-type values, and add unit tests for both
behaviors.
Does some unit test cleanup for the operators unit tests to make them easier to
write.
BUG=
Review-Url: https://codereview.chromium.org/2224343003
Cr-Original-Commit-Position: refs/heads/master@{#410874}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: 56350de24ed370f955b299022e93229f8643ace8
diff --git a/tools/gn/operators.cc b/tools/gn/operators.cc
index ac6a37f..9ac18bf 100644
--- a/tools/gn/operators.cc
+++ b/tools/gn/operators.cc
@@ -209,6 +209,33 @@
*err = Err(*name_token_, "Undefined identifier.");
}
+// Computes an error message for overwriting a nonempty list/scope with another.
+Err MakeOverwriteError(const BinaryOpNode* op_node,
+ const Value& old_value) {
+ std::string type_name;
+ std::string empty_def;
+
+ if (old_value.type() == Value::LIST) {
+ type_name = "list";
+ empty_def = "[]";
+ } else if (old_value.type() == Value::SCOPE) {
+ type_name = "scope";
+ empty_def = "{}";
+ } else {
+ NOTREACHED();
+ }
+
+ Err result(op_node->left()->GetRange(),
+ "Replacing nonempty " + type_name + ".",
+ "This overwrites a previously-defined nonempty " + type_name +
+ "with another nonempty " + type_name + ".");
+ result.AppendSubErr(Err(old_value, "for previous definition",
+ "Did you mean to append/modify instead? If you really want to overwrite, "
+ "do:\n"
+ " foo = " + empty_def + "\nbefore reassigning."));
+ return result;
+}
+
// -----------------------------------------------------------------------------
Err MakeIncompatibleTypeError(const BinaryOpNode* op_node,
@@ -297,31 +324,23 @@
Err* err) {
const Value* old_value = dest->GetExistingValue();
if (old_value) {
- // Throw an error when overwriting a nonempty list with another nonempty
- // list item. This is to detect the case where you write
- // defines = ["FOO"]
- // and you overwrote inherited ones, when instead you mean to append:
- // defines += ["FOO"]
- if (old_value->type() == Value::LIST &&
- !old_value->list_value().empty() &&
- right.type() == Value::LIST &&
- !right.list_value().empty()) {
- *err = Err(op_node->left()->GetRange(), "Replacing nonempty list.",
- std::string("This overwrites a previously-defined nonempty list ") +
- "(length " +
- base::IntToString(static_cast<int>(old_value->list_value().size()))
- + ").");
- err->AppendSubErr(Err(*old_value, "for previous definition",
- "with another one (length " +
- base::IntToString(static_cast<int>(right.list_value().size())) +
- "). Did you mean " +
- "\"+=\" to append instead? If you\nreally want to do this, do\n"
- " foo = []\nbefore reassigning."));
+ // Check for overwriting nonempty scopes or lists with other nonempty
+ // scopes or lists. This prevents mistakes that clobber a value rather than
+ // appending to it. For cases where a user meant to clear a value, allow
+ // overwriting a nonempty list/scope with an empty one, which can then be
+ // modified.
+ if (old_value->type() == Value::LIST && right.type() == Value::LIST &&
+ !old_value->list_value().empty() && !right.list_value().empty()) {
+ *err = MakeOverwriteError(op_node, *old_value);
+ return Value();
+ } else if (old_value->type() == Value::SCOPE &&
+ right.type() == Value::SCOPE &&
+ old_value->scope_value()->HasValues(Scope::SEARCH_CURRENT) &&
+ right.scope_value()->HasValues(Scope::SEARCH_CURRENT)) {
+ *err = MakeOverwriteError(op_node, *old_value);
return Value();
}
}
- if (err->has_error())
- return Value();
Value* written_value = dest->SetValue(std::move(right), op_node->right());
diff --git a/tools/gn/operators_unittest.cc b/tools/gn/operators_unittest.cc
index e2c396c..4fa3d11 100644
--- a/tools/gn/operators_unittest.cc
+++ b/tools/gn/operators_unittest.cc
@@ -26,15 +26,6 @@
return v.string_value() == s;
}
-// Returns a list populated with a single literal Value corresponding to the
-// given token. The token must outlive the list (since the list will just
-// copy the reference).
-std::unique_ptr<ListNode> ListWithLiteral(const Token& token) {
- std::unique_ptr<ListNode> list(new ListNode);
- list->append_item(std::unique_ptr<ParseNode>(new LiteralNode(token)));
- return list;
-}
-
// This parse node is for passing to tests. It returns a canned value for
// Execute().
class TestParseNode : public ParseNode {
@@ -59,6 +50,54 @@
Value value_;
};
+// Sets up a BinaryOpNode for testing.
+class TestBinaryOpNode : public BinaryOpNode {
+ public:
+ // Input token value string must outlive class.
+ TestBinaryOpNode(Token::Type op_token_type,
+ const char* op_token_value)
+ : BinaryOpNode(),
+ op_token_ownership_(Location(), op_token_type, op_token_value) {
+ set_op(op_token_ownership_);
+ }
+
+ void SetLeftToValue(const Value& value) {
+ set_left(std::unique_ptr<ParseNode>(new TestParseNode(value)));
+ }
+
+ // Sets the left-hand side of the operator to an identifier node, this is
+ // used for testing assignments. Input string must outlive class.
+ void SetLeftToIdentifier(const char* identifier) {
+ left_identifier_token_ownership_ =
+ Token(Location(), Token::IDENTIFIER, identifier);
+ set_left(std::unique_ptr<ParseNode>(
+ new IdentifierNode(left_identifier_token_ownership_)));
+ }
+
+ void SetRightToValue(const Value& value) {
+ set_right(std::unique_ptr<ParseNode>(new TestParseNode(value)));
+ }
+ void SetRightToListOfValue(const Value& value) {
+ Value list(nullptr, Value::LIST);
+ list.list_value().push_back(value);
+ set_right(std::unique_ptr<ParseNode>(new TestParseNode(list)));
+ }
+ void SetRightToListOfValue(const Value& value1, const Value& value2) {
+ Value list(nullptr, Value::LIST);
+ list.list_value().push_back(value1);
+ list.list_value().push_back(value2);
+ set_right(std::unique_ptr<ParseNode>(new TestParseNode(list)));
+ }
+
+ private:
+ // The base class takes the Token by reference, this manages the lifetime.
+ Token op_token_ownership_;
+
+ // When setting the left to an identifier, this manages the lifetime of
+ // the identifier token.
+ Token left_identifier_token_ownership_;
+};
+
} // namespace
TEST(Operators, SourcesAppend) {
@@ -70,15 +109,8 @@
setup.scope()->SetValue(sources, Value(nullptr, Value::LIST), nullptr);
// Set up the operator.
- BinaryOpNode node;
- const char token_value[] = "+=";
- Token op(Location(), Token::PLUS_EQUALS, token_value);
- node.set_op(op);
-
- // Append to the sources variable.
- Token identifier_token(Location(), Token::IDENTIFIER, sources);
- node.set_left(
- std::unique_ptr<ParseNode>(new IdentifierNode(identifier_token)));
+ TestBinaryOpNode node(Token::PLUS_EQUALS, "+=");
+ node.SetLeftToIdentifier(sources);
// Set up the filter on the scope to remove everything ending with "rm"
std::unique_ptr<PatternList> pattern_list(new PatternList);
@@ -86,31 +118,25 @@
setup.scope()->set_sources_assignment_filter(std::move(pattern_list));
// Append an integer.
- const char integer_value[] = "5";
- Token integer(Location(), Token::INTEGER, integer_value);
- node.set_right(ListWithLiteral(integer));
+ node.SetRightToListOfValue(Value(nullptr, static_cast<int64_t>(5)));
node.Execute(setup.scope(), &err);
EXPECT_FALSE(err.has_error());
// Append a string that doesn't match the pattern, it should get appended.
- const char string_1_value[] = "\"good\"";
- Token string_1(Location(), Token::STRING, string_1_value);
- node.set_right(ListWithLiteral(string_1));
+ const char string1[] = "good";
+ node.SetRightToListOfValue(Value(nullptr, string1));
node.Execute(setup.scope(), &err);
EXPECT_FALSE(err.has_error());
// Append a string that does match the pattern, it should be a no-op.
- const char string_2_value[] = "\"foo-rm\"";
- Token string_2(Location(), Token::STRING, string_2_value);
- node.set_right(ListWithLiteral(string_2));
+ const char string2[] = "foo-rm";
+ node.SetRightToListOfValue(Value(nullptr, string2));
node.Execute(setup.scope(), &err);
EXPECT_FALSE(err.has_error());
// Append a list with the two strings from above.
- ListNode list;
- list.append_item(std::unique_ptr<ParseNode>(new LiteralNode(string_1)));
- list.append_item(std::unique_ptr<ParseNode>(new LiteralNode(string_2)));
- ExecuteBinaryOperator(setup.scope(), &node, node.left(), &list, &err);
+ node.SetRightToListOfValue(Value(nullptr, string1), Value(nullptr, string2));
+ node.Execute(setup.scope(), &err);
EXPECT_FALSE(err.has_error());
// The sources variable in the scope should now have: [ 5, "good", "good" ]
@@ -133,23 +159,14 @@
const char foo[] = "foo";
setup.scope()->SetValue(foo, Value(nullptr, Value::LIST), nullptr);
- // Set up the operator.
- BinaryOpNode node;
- const char token_value[] = "+=";
- Token op(Location(), Token::PLUS_EQUALS, token_value);
- node.set_op(op);
-
- // Append to the foo variable.
- Token identifier_token(Location(), Token::IDENTIFIER, foo);
- node.set_left(
- std::unique_ptr<ParseNode>(new IdentifierNode(identifier_token)));
+ // Set up the operator to append to "foo".
+ TestBinaryOpNode node(Token::PLUS_EQUALS, "+=");
+ node.SetLeftToIdentifier(foo);
// Append a list with a list, the result should be a nested list.
- std::unique_ptr<ListNode> outer_list(new ListNode);
- const char twelve_str[] = "12";
- Token twelve(Location(), Token::INTEGER, twelve_str);
- outer_list->append_item(ListWithLiteral(twelve));
- node.set_right(std::move(outer_list));
+ Value inner_list(nullptr, Value::LIST);
+ inner_list.list_value().push_back(Value(nullptr, static_cast<int64_t>(12)));
+ node.SetRightToListOfValue(inner_list);
Value ret = ExecuteBinaryOperator(setup.scope(), &node, node.left(),
node.right(), &err);
@@ -177,7 +194,7 @@
EXPECT_TRUE(err.has_error());
err = Err();
- node.set_right(std::unique_ptr<ParseNode>(new LiteralNode(twelve)));
+ node.SetRightToValue(Value(nullptr, static_cast<int64_t>(12)));
ExecuteBinaryOperator(setup.scope(), &node, node.left(), node.right(), &err);
EXPECT_TRUE(err.has_error());
}
@@ -194,26 +211,14 @@
test_list.list_value().push_back(Value(nullptr, foo_str));
// Set up "var" with an the test list.
- const char var_str[] = "var";
- setup.scope()->SetValue(var_str, test_list, nullptr);
+ const char var[] = "var";
+ setup.scope()->SetValue(var, test_list, nullptr);
- // Set up the operator.
- BinaryOpNode node;
- const char token_value[] = "-=";
- Token op(Location(), Token::MINUS_EQUALS, token_value);
- node.set_op(op);
-
- // Do -= on the var.
- Token identifier_token(Location(), Token::IDENTIFIER, var_str);
- node.set_left(
- std::unique_ptr<ParseNode>(new IdentifierNode(identifier_token)));
+ TestBinaryOpNode node(Token::MINUS_EQUALS, "-=");
+ node.SetLeftToIdentifier(var);
// Subtract a list consisting of "foo".
- Value foo_list(nullptr, Value::LIST);
- foo_list.list_value().push_back(Value(nullptr, foo_str));
- std::unique_ptr<ParseNode> outer_list(new TestParseNode(foo_list));
- node.set_right(std::move(outer_list));
-
+ node.SetRightToListOfValue(Value(nullptr, foo_str));
Value result = ExecuteBinaryOperator(
setup.scope(), &node, node.left(), node.right(), &err);
EXPECT_FALSE(err.has_error());
@@ -224,7 +229,7 @@
// The "var" variable should have been updated. Both instances of "foo" are
// deleted.
- const Value* new_value = setup.scope()->GetValue(var_str);
+ const Value* new_value = setup.scope()->GetValue(var);
ASSERT_TRUE(new_value);
ASSERT_EQ(Value::LIST, new_value->type());
ASSERT_EQ(1u, new_value->list_value().size());
@@ -236,16 +241,9 @@
Err err;
TestWithScope setup;
- // Set up the operator.
- BinaryOpNode node;
- const char token_value[] = "&&";
- Token op(Location(), Token::BOOLEAN_AND, token_value);
- node.set_op(op);
-
- // Set the left to false.
- const char false_str[] = "false";
- Token false_tok(Location(), Token::FALSE_TOKEN, false_str);
- node.set_left(std::unique_ptr<ParseNode>(new LiteralNode(false_tok)));
+ // Set a && operator with the left to false.
+ TestBinaryOpNode node(Token::BOOLEAN_AND, "&&");
+ node.SetLeftToValue(Value(nullptr, false));
// Set right as foo, but don't define a value for it.
const char foo[] = "foo";
@@ -262,16 +260,9 @@
Err err;
TestWithScope setup;
- // Set up the operator.
- BinaryOpNode node;
- const char token_value[] = "||";
- Token op(Location(), Token::BOOLEAN_OR, token_value);
- node.set_op(op);
-
- // Set the left to false.
- const char false_str[] = "true";
- Token false_tok(Location(), Token::TRUE_TOKEN, false_str);
- node.set_left(std::unique_ptr<ParseNode>(new LiteralNode(false_tok)));
+ // Set a || operator with the left to true.
+ TestBinaryOpNode node(Token::BOOLEAN_OR, "||");
+ node.SetLeftToValue(Value(nullptr, true));
// Set right as foo, but don't define a value for it.
const char foo[] = "foo";
@@ -283,3 +274,59 @@
node.right(), &err);
EXPECT_FALSE(err.has_error());
}
+
+// Overwriting nonempty lists and scopes with other nonempty lists and scopes
+// should be disallowed.
+TEST(Operators, NonemptyOverwriting) {
+ Err err;
+ TestWithScope setup;
+
+ // Set up "foo" with a nonempty list.
+ const char foo[] = "foo";
+ Value old_value(nullptr, Value::LIST);
+ old_value.list_value().push_back(Value(nullptr, "string"));
+ setup.scope()->SetValue(foo, old_value, nullptr);
+
+ TestBinaryOpNode node(Token::EQUAL, "=");
+ node.SetLeftToIdentifier(foo);
+
+ // Assigning a nonempty list should fail.
+ node.SetRightToListOfValue(Value(nullptr, "string"));
+ node.Execute(setup.scope(), &err);
+ ASSERT_TRUE(err.has_error());
+ EXPECT_EQ("Replacing nonempty list.", err.message());
+ err = Err();
+
+ // Assigning an empty list should succeed.
+ node.SetRightToValue(Value(nullptr, Value::LIST));
+ node.Execute(setup.scope(), &err);
+ ASSERT_FALSE(err.has_error());
+ const Value* new_value = setup.scope()->GetValue(foo);
+ ASSERT_TRUE(new_value);
+ ASSERT_EQ(Value::LIST, new_value->type());
+ ASSERT_TRUE(new_value->list_value().empty());
+
+ // Set up "foo" with a nonempty scope.
+ const char bar[] = "bar";
+ old_value = Value(
+ nullptr, std::unique_ptr<Scope>(new Scope(setup.settings())));
+ old_value.scope_value()->SetValue(bar, Value(nullptr, "bar"), nullptr);
+ setup.scope()->SetValue(foo, old_value, nullptr);
+
+ // Assigning a nonempty scope should fail (re-use old_value copy).
+ node.SetRightToValue(old_value);
+ node.Execute(setup.scope(), &err);
+ ASSERT_TRUE(err.has_error());
+ EXPECT_EQ("Replacing nonempty scope.", err.message());
+ err = Err();
+
+ // Assigning an empty list should succeed.
+ node.SetRightToValue(
+ Value(nullptr, std::unique_ptr<Scope>(new Scope(setup.settings()))));
+ node.Execute(setup.scope(), &err);
+ ASSERT_FALSE(err.has_error());
+ new_value = setup.scope()->GetValue(foo);
+ ASSERT_TRUE(new_value);
+ ASSERT_EQ(Value::SCOPE, new_value->type());
+ ASSERT_FALSE(new_value->scope_value()->HasValues(Scope::SEARCH_CURRENT));
+}
diff --git a/tools/gn/scope.cc b/tools/gn/scope.cc
index 7e7c201..fbe73f4 100644
--- a/tools/gn/scope.cc
+++ b/tools/gn/scope.cc
@@ -71,6 +71,11 @@
mutable_containing_ = nullptr;
}
+bool Scope::HasValues(SearchNested search_nested) const {
+ DCHECK(search_nested == SEARCH_CURRENT);
+ return !values_.empty();
+}
+
const Value* Scope::GetValue(const base::StringPiece& ident,
bool counts_as_used) {
// First check for programmatically-provided values.
diff --git a/tools/gn/scope.h b/tools/gn/scope.h
index 3d36712..b87afcd 100644
--- a/tools/gn/scope.h
+++ b/tools/gn/scope.h
@@ -125,6 +125,14 @@
// self-sufficient.
void DetachFromContaining();
+ // Returns true if the scope has any values set. This does not check other
+ // things that may be set like templates or defaults.
+ //
+ // Currently this does not search nested scopes and this will assert if you
+ // want to search nested scopes. The enum is passed so the callers are
+ // unambiguous about nested scope handling. This can be added if needed.
+ bool HasValues(SearchNested search_nested) const;
+
// Returns NULL if there's no such value.
//
// counts_as_used should be set if the variable is being read in a way that
@@ -133,16 +141,6 @@
bool counts_as_used);
const Value* GetValue(const base::StringPiece& ident) const;
- // If the value exists in the current scope, destrictively moves it into the
- // return value. If it exists in a containing scope, copies it.
- //
- // This is for implementing modify-write operations where we want to read
- // the existing value and plan to immediately overwrite it. If the value is
- // in a containing scope, we never want to touch it (all writes go to the
- // current scope), but if it's in the current scope, avoid the copy since it
- // will be overwritten anyway.
- //Value DestructiveMoveOut(const base::StringPiece& ident);
-
// Returns the requested value as a mutable one if possible. If the value
// is not found in a mutable scope, then returns null. Note that the value
// could still exist in a const scope, so GetValue() could still return