[Skylark] Allow tuples as first argument of str.{starts,ends}with
Closes #5307
Closes #5455.
PiperOrigin-RevId: 202567483
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 e18c833..7d4cbb6 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
@@ -19,6 +19,7 @@
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.skylarkinterface.Param;
+import com.google.devtools.build.lib.skylarkinterface.ParamType;
import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModuleCategory;
@@ -893,7 +894,13 @@
+ "and <code>end</code> being exclusive.",
parameters = {
@Param(name = "self", type = String.class, doc = "This string."),
- @Param(name = "sub", type = String.class, legacyNamed = true,
+ @Param(
+ name = "sub",
+ allowedTypes = {
+ @ParamType(type = String.class),
+ @ParamType(type = Tuple.class, generic1 = String.class),
+ },
+ legacyNamed = true,
doc = "The substring to check."),
@Param(
name = "start",
@@ -909,9 +916,21 @@
defaultValue = "None",
doc = "optional position at which to stop comparing.")
})
- public Boolean endsWith(String self, String sub, Integer start, Object end)
- throws ConversionException {
- return pythonSubstring(self, start, end, "'end' operand of 'endswith'").endsWith(sub);
+ public Boolean endsWith(String self, Object sub, Integer start, Object end)
+ throws ConversionException, EvalException {
+ String str = pythonSubstring(self, start, end, "'end' operand of 'endswith'");
+ if (sub instanceof String) {
+ return str.endsWith((String) sub);
+ }
+
+ @SuppressWarnings("unchecked")
+ Tuple<Object> subs = (Tuple<Object>) sub;
+ for (String s : subs.getContents(String.class, "string")) {
+ if (str.endsWith(s)) {
+ return true;
+ }
+ }
+ return false;
}
// In Python, formatting is very complex.
@@ -966,8 +985,14 @@
+ "<code>end</code> being exclusive.",
parameters = {
@Param(name = "self", type = String.class, doc = "This string."),
- @Param(name = "sub", type = String.class, legacyNamed = true,
- doc = "The substring to check."),
+ @Param(
+ name = "sub",
+ allowedTypes = {
+ @ParamType(type = String.class),
+ @ParamType(type = Tuple.class, generic1 = String.class),
+ },
+ legacyNamed = true,
+ doc = "The substring(s) to check."),
@Param(
name = "start",
type = Integer.class,
@@ -982,9 +1007,21 @@
defaultValue = "None",
doc = "Stop comparing at this position.")
})
- public Boolean startsWith(String self, String sub, Integer start, Object end)
- throws ConversionException {
- return pythonSubstring(self, start, end, "'end' operand of 'startswith'").startsWith(sub);
+ public Boolean startsWith(String self, Object sub, Integer start, Object end)
+ throws ConversionException, EvalException {
+ String str = pythonSubstring(self, start, end, "'end' operand of 'startswith'");
+ if (sub instanceof String) {
+ return str.startsWith((String) sub);
+ }
+
+ @SuppressWarnings("unchecked")
+ Tuple<Object> subs = (Tuple<Object>) sub;
+ for (String s : subs.getContents(String.class, "string")) {
+ if (str.startsWith(s)) {
+ return true;
+ }
+ }
+ return false;
}
public static final StringModule INSTANCE = new StringModule();
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java b/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java
index 7420cde..072b9c8 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java
@@ -149,7 +149,7 @@
new BothModesTest()
.testIfErrorContains("substring \"z\" not found in \"abc\"", "'abc'.index('z')")
.testIfErrorContains(
- "expected value of type 'string' for parameter 'sub', "
+ "expected value of type 'string or tuple of strings' for parameter 'sub', "
+ "in method call startswith(int) of 'string'",
"'test'.startswith(1)")
.testIfErrorContains(
diff --git a/src/test/skylark/testdata/string_misc.sky b/src/test/skylark/testdata/string_misc.sky
index 901d7f3..87708f9 100644
--- a/src/test/skylark/testdata/string_misc.sky
+++ b/src/test/skylark/testdata/string_misc.sky
@@ -62,6 +62,26 @@
assert_eq('abcd'.endswith('c', -2, -1), True)
assert_eq('abcd'.endswith('c', 1, 8), False)
assert_eq('abcd'.endswith('d', 1, 8), True)
+assert_eq('Apricot'.endswith(('cot', 'toc')), True)
+assert_eq('Apricot'.endswith(('toc', 'cot')), True)
+assert_eq('a'.endswith(('', '')), True)
+assert_eq('a'.endswith(('', 'a')), True)
+assert_eq('a'.endswith(('a', 'a')), True)
+assert_eq(''.endswith(('a', '')), True)
+assert_eq(''.endswith(('', '')), True)
+assert_eq(''.endswith(('a', 'a')), False)
+assert_eq('a'.endswith(('a')), True)
+assert_eq('a'.endswith(('a',)), True)
+assert_eq('a'.endswith(('b',)), False)
+assert_eq('a'.endswith(()), False)
+assert_eq(''.endswith(()), False)
+---
+'a'.endswith(['a']) ### expected value of type 'string or tuple of strings' for parameter 'sub', in method call endswith(list) of 'string'
+---
+'1'.endswith((1,)) ### expected type 'string' for 'string' element but got type 'int' instead
+---
+'a'.endswith(('1', 1)) ### expected type 'string' for 'string' element but got type 'int' instead
+---
# startswith
assert_eq('Apricot'.startswith('Apr'), True)
@@ -70,6 +90,26 @@
assert_eq('Apricot'.startswith('z'), False)
assert_eq(''.startswith(''), True)
assert_eq(''.startswith('a'), False)
+assert_eq('Apricot'.startswith(('Apr', 'rpA')), True)
+assert_eq('Apricot'.startswith(('rpA', 'Apr')), True)
+assert_eq('a'.startswith(('', '')), True)
+assert_eq('a'.startswith(('', 'a')), True)
+assert_eq('a'.startswith(('a', 'a')), True)
+assert_eq(''.startswith(('a', '')), True)
+assert_eq(''.startswith(('', '')), True)
+assert_eq(''.startswith(('a', 'a')), False)
+assert_eq('a'.startswith(('a')), True)
+assert_eq('a'.startswith(('a',)), True)
+assert_eq('a'.startswith(('b',)), False)
+assert_eq('a'.startswith(()), False)
+assert_eq(''.startswith(()), False)
+---
+'a'.startswith(['a']) ### expected value of type 'string or tuple of strings' for parameter 'sub', in method call startswith(list) of 'string'
+---
+'1'.startswith((1,)) ### expected type 'string' for 'string' element but got type 'int' instead
+---
+'a'.startswith(('1', 1)) ### expected type 'string' for 'string' element but got type 'int' instead
+---
# substring
assert_eq('012345678'[0:-1], "01234567")