Handle negative count arg in string.replace
Also update the name of the argument to be "count" instead of "maxsplit",
to match the Starlark spec.
This fixes #9181
Closes #9228.
PiperOrigin-RevId: 309400161
diff --git a/src/main/java/com/google/devtools/build/lib/packages/StarlarkSemanticsOptions.java b/src/main/java/com/google/devtools/build/lib/packages/StarlarkSemanticsOptions.java
index 0176933..5318010 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/StarlarkSemanticsOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/StarlarkSemanticsOptions.java
@@ -508,6 +508,18 @@
help = "If set to true, the command parameter of actions.run_shell will only accept string")
public boolean incompatibleRunShellCommandString;
+ @Option(
+ name = "incompatible_string_replace_count",
+ defaultValue = "false",
+ documentationCategory = OptionDocumentationCategory.STARLARK_SEMANTICS,
+ effectTags = {OptionEffectTag.BUILD_FILE_SEMANTICS},
+ metadataTags = {
+ OptionMetadataTag.INCOMPATIBLE_CHANGE,
+ OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES
+ },
+ help = "If set to true, a negative count in string.replace() will be ignored")
+ public boolean incompatibleStringReplaceCount;
+
/** Used in an integration test to confirm that flags are visible to the interpreter. */
@Option(
name = "internal_skylark_flag_test_canary",
@@ -652,6 +664,7 @@
.incompatibleNoRuleOutputsParam(incompatibleNoRuleOutputsParam)
.incompatibleNoSupportToolsInActionInputs(incompatibleNoSupportToolsInActionInputs)
.incompatibleRunShellCommandString(incompatibleRunShellCommandString)
+ .incompatibleStringReplaceCount(incompatibleStringReplaceCount)
.incompatibleVisibilityPrivateAttributesAtDefinition(
incompatibleVisibilityPrivateAttributesAtDefinition)
.internalSkylarkFlagTestCanary(internalSkylarkFlagTestCanary)
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/StarlarkSemantics.java b/src/main/java/com/google/devtools/build/lib/syntax/StarlarkSemantics.java
index 76c517d..a1d9e2b 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/StarlarkSemantics.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/StarlarkSemantics.java
@@ -257,6 +257,8 @@
public abstract boolean incompatibleRunShellCommandString();
+ public abstract boolean incompatibleStringReplaceCount();
+
public abstract boolean incompatibleVisibilityPrivateAttributesAtDefinition();
public abstract boolean internalSkylarkFlagTestCanary();
@@ -343,6 +345,7 @@
.incompatibleNoRuleOutputsParam(false)
.incompatibleNoSupportToolsInActionInputs(true)
.incompatibleRunShellCommandString(false)
+ .incompatibleStringReplaceCount(false)
.incompatibleVisibilityPrivateAttributesAtDefinition(false)
.internalSkylarkFlagTestCanary(false)
.incompatibleDoNotSplitLinkingCmdline(true)
@@ -422,6 +425,8 @@
public abstract Builder incompatibleRunShellCommandString(boolean value);
+ public abstract Builder incompatibleStringReplaceCount(boolean value);
+
public abstract Builder incompatibleVisibilityPrivateAttributesAtDefinition(boolean value);
public abstract Builder internalSkylarkFlagTestCanary(boolean value);
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/StringModule.java b/src/main/java/com/google/devtools/build/lib/syntax/StringModule.java
index f46ae15..32d8bd8 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/StringModule.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/StringModule.java
@@ -263,30 +263,39 @@
+ "restricting the number of replacements to <code>maxsplit</code>.",
parameters = {
@Param(name = "self", type = String.class, doc = "This string."),
+ @Param(name = "old", type = String.class, doc = "The string to be replaced."),
+ @Param(name = "new", type = String.class, doc = "The string to replace with."),
@Param(
- name = "old",
- type = String.class,
- doc = "The string to be replaced."),
- @Param(
- name = "new",
- type = String.class,
- doc = "The string to replace with."),
- @Param(
+ // TODO(#8147): rename param to "count" once it's positional-only.
name = "maxsplit",
type = Integer.class,
noneable = true,
defaultValue = "None",
- doc = "The maximum number of replacements.")
- })
- public String replace(String self, String oldString, String newString, Object maxSplitO)
+ doc =
+ "The maximum number of replacements. A negative value is ignored if"
+ + " --incompatible_string_replace_count is true; otherwise a negative value"
+ + " is treated as 0.")
+ },
+ useStarlarkThread = true)
+ public String replace(
+ String self, String oldString, String newString, Object count, StarlarkThread thread)
throws EvalException {
- int maxSplit = Integer.MAX_VALUE;
- if (maxSplitO != Starlark.NONE) {
- maxSplit = Math.max(0, (Integer) maxSplitO);
+ int maxReplaces = Integer.MAX_VALUE;
+
+ StarlarkSemantics semantics = thread.getSemantics();
+ if (semantics.incompatibleStringReplaceCount()) {
+ if (count != Starlark.NONE && (Integer) count >= 0) {
+ maxReplaces = (Integer) count;
+ }
+ } else {
+ if (count != Starlark.NONE) {
+ maxReplaces = Math.max(0, (Integer) count);
+ }
}
+
StringBuilder sb = new StringBuilder();
int start = 0;
- for (int i = 0; i < maxSplit; i++) {
+ for (int i = 0; i < maxReplaces; i++) {
if (oldString.isEmpty()) {
sb.append(newString);
if (start < self.length()) {
diff --git a/src/test/java/com/google/devtools/build/lib/packages/SkylarkSemanticsConsistencyTest.java b/src/test/java/com/google/devtools/build/lib/packages/SkylarkSemanticsConsistencyTest.java
index 3539c6f..1aeb408 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/SkylarkSemanticsConsistencyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/SkylarkSemanticsConsistencyTest.java
@@ -156,6 +156,7 @@
"--incompatible_no_rule_outputs_param=" + rand.nextBoolean(),
"--incompatible_no_support_tools_in_action_inputs=" + rand.nextBoolean(),
"--incompatible_run_shell_command_string=" + rand.nextBoolean(),
+ "--incompatible_string_replace_count=" + rand.nextBoolean(),
"--incompatible_visibility_private_attributes_at_definition=" + rand.nextBoolean(),
"--incompatible_require_linker_input_cc_api=" + rand.nextBoolean(),
"--incompatible_restrict_string_escapes=" + rand.nextBoolean(),
@@ -207,6 +208,7 @@
.incompatibleNoRuleOutputsParam(rand.nextBoolean())
.incompatibleNoSupportToolsInActionInputs(rand.nextBoolean())
.incompatibleRunShellCommandString(rand.nextBoolean())
+ .incompatibleStringReplaceCount(rand.nextBoolean())
.incompatibleVisibilityPrivateAttributesAtDefinition(rand.nextBoolean())
.incompatibleRequireLinkerInputCcApi(rand.nextBoolean())
.incompatibleRestrictStringEscapes(rand.nextBoolean())
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/StringModuleTest.java b/src/test/java/com/google/devtools/build/lib/syntax/StringModuleTest.java
new file mode 100644
index 0000000..dbc7b55
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/StringModuleTest.java
@@ -0,0 +1,76 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.syntax;
+
+import com.google.devtools.build.lib.syntax.util.EvaluationTestCase;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+/** Tests for SkylarkStringModule. */
+// TODO(bazel-team): Migrate these test cases back to string_misc.sky, once either
+// 1) --incompatible_string_replace_count has been flipped (#11244) and deleted, or 2) the
+// standalone Starlark interpreter and tests gain the ability to run with semantics flags.
+@RunWith(Parameterized.class)
+public class StringModuleTest extends EvaluationTestCase {
+
+ @Parameters(name = "{index}: flag={0}")
+ public static Iterable<? extends Object> data() {
+ return Arrays.asList(
+ "--incompatible_string_replace_count=false", "--incompatible_string_replace_count=true");
+ }
+
+ @Parameter public String flag;
+
+ @Test
+ public void testReplace() throws Exception {
+ // Test that the behaviour is the same for the basic case both before
+ // and after the incompatible change.
+ new Scenario(flag)
+ .testEval("'banana'.replace('a', 'o')", "'bonono'")
+ .testEval("'banana'.replace('a', 'o', 2)", "'bonona'")
+ .testEval("'banana'.replace('a', 'o', 0)", "'banana'")
+ .testEval("'banana'.replace('a', 'e')", "'benene'")
+ .testEval("'banana'.replace('a', '$()')", "'b$()n$()n$()'")
+ .testEval("'banana'.replace('a', '$')", "'b$n$n$'")
+ .testEval("'b$()n$()n$()'.replace('$()', '$($())')", "'b$($())n$($())n$($())'")
+ .testEval("'banana'.replace('a', 'e', 2)", "'benena'")
+ .testEval("'banana'.replace('a', 'e', 0)", "'banana'")
+ .testEval("'banana'.replace('', '-')", "'-b-a-n-a-n-a-'")
+ .testEval("'banana'.replace('', '-', 2)", "'-b-anana'")
+ .testEval("'banana'.replace('', '-', 0)", "'banana'")
+ .testEval("'banana'.replace('', '')", "'banana'")
+ .testEval("'banana'.replace('a', '')", "'bnn'")
+ .testEval("'banana'.replace('a', '', 2)", "'bnna'");
+ }
+
+ @Test
+ public void testReplaceIncompatibleFlag() throws Exception {
+ // Test the scenario that changes with the incompatible flag
+ new Scenario("--incompatible_string_replace_count=false")
+ .testEval("'banana'.replace('a', 'o', -2)", "'banana'")
+ .testEval("'banana'.replace('a', 'e', -1)", "'banana'")
+ .testEval("'banana'.replace('a', 'e', -10)", "'banana'")
+ .testEval("'banana'.replace('', '-', -2)", "'banana'");
+
+ new Scenario("--incompatible_string_replace_count=true")
+ .testEval("'banana'.replace('a', 'o', -2)", "'bonono'")
+ .testEval("'banana'.replace('a', 'e', -1)", "'benene'")
+ .testEval("'banana'.replace('a', 'e', -10)", "'benene'")
+ .testEval("'banana'.replace('', '-', -2)", "'-b-a-n-a-n-a-'");
+ }
+}
diff --git a/src/test/starlark/testdata/string_misc.sky b/src/test/starlark/testdata/string_misc.sky
index ea3aa9d..d3286ab 100644
--- a/src/test/starlark/testdata/string_misc.sky
+++ b/src/test/starlark/testdata/string_misc.sky
@@ -43,11 +43,7 @@
assert_eq('banana'.replace('a', 'e', 2), "benena")
assert_eq('banana'.replace('a', 'e', 0), "banana")
-assert_eq('banana'.replace('a', 'e', -1), "banana")
-assert_eq('banana'.replace('a', 'e', -10), "banana")
-assert_eq('banana'.replace('', '-'), "-b-a-n-a-n-a-")
-assert_eq('banana'.replace('', '-', -2), "banana")
assert_eq('banana'.replace('', '-', 2), "-b-anana")
assert_eq('banana'.replace('', '-', 0), "banana")