Skylark: int() can now declare a base parameter. -- MOS_MIGRATED_REVID=126434299
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java b/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java index ad52534..f02b585 100644 --- a/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java +++ b/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java
@@ -17,6 +17,7 @@ import com.google.common.base.CharMatcher; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; @@ -1762,33 +1763,92 @@ } }; - @SkylarkSignature(name = "int", returnType = Integer.class, doc = "Converts a value to int. " - + "If the argument is a string, it is converted using base 10 and raises an error if the " - + "conversion fails. If the argument is a bool, it returns 0 (False) or 1 (True). " - + "If the argument is an int, it is simply returned." - + "<pre class=\"language-python\">int(\"123\") == 123</pre>", - parameters = { - @Param(name = "x", type = Object.class, doc = "The string to convert.")}, - useLocation = true) - private static final BuiltinFunction int_ = new BuiltinFunction("int") { - public Integer invoke(Object x, Location loc) throws EvalException { - if (x instanceof Boolean) { - return ((Boolean) x).booleanValue() ? 1 : 0; - } else if (x instanceof Integer) { - return (Integer) x; - } else if (x instanceof String) { - try { - return Integer.parseInt((String) x); - } catch (NumberFormatException e) { - throw new EvalException(loc, - "invalid literal for int(): " + Printer.repr(x)); + @SkylarkSignature( + name = "int", + returnType = Integer.class, + doc = + "Converts a value to int. " + + "If the argument is a string, it is converted using the given base and raises an " + + "error if the conversion fails. " + + "The base can be between 2 and 36 (inclusive) and defaults to 10. " + + "The value can be prefixed with 0b/0o/ox to represent values in base 2/8/16. " + + "If such a prefix is present, a base of 0 can be used to automatically determine the " + + "correct base: " + + "<pre class=\"language-python\">int(\"0xFF\", 0) == int(\"0xFF\", 16) == 255</pre>" + + "If the argument is a bool, it returns 0 (False) or 1 (True). " + + "If the argument is an int, it is simply returned." + + "<pre class=\"language-python\">int(\"123\") == 123</pre>", + parameters = { + @Param(name = "x", type = Object.class, doc = "The string to convert."), + @Param( + name = "base", + type = Integer.class, + defaultValue = "10", + doc = "The base of the string." + ) + }, + useLocation = true + ) + private static final BuiltinFunction int_ = + new BuiltinFunction("int") { + private final ImmutableMap<String, Integer> intPrefixes = + ImmutableMap.of("0b", 2, "0o", 8, "0x", 16); + + @SuppressWarnings("unused") + public Integer invoke(Object x, Integer base, Location loc) throws EvalException { + if (x instanceof String) { + return fromString(x, loc, base); + } else { + if (base != 10) { + throw new EvalException(loc, "int() can't convert non-string with explicit base"); + } + if (x instanceof Boolean) { + return ((Boolean) x).booleanValue() ? 1 : 0; + } else if (x instanceof Integer) { + return (Integer) x; + } + throw new EvalException( + loc, Printer.format("%r is not of type string or int or bool", x)); + } } - } else { - throw new EvalException(loc, - Printer.format("%r is not of type string or int or bool", x)); - } - } - }; + + private int fromString(Object x, Location loc, int base) throws EvalException { + String value = (String) x; + String prefix = getIntegerPrefix(value); + + if (!prefix.isEmpty()) { + value = value.substring(prefix.length()); + int expectedBase = intPrefixes.get(prefix); + if (base == 0) { + // Similar to Python, base 0 means "derive the base from the prefix". + base = expectedBase; + } else if (base != expectedBase) { + throw new EvalException( + loc, Printer.format("invalid literal for int() with base %d: %r", base, x)); + } + } + + if (base < 2 || base > 36) { + throw new EvalException(loc, "int() base must be >= 2 and <= 36"); + } + try { + return Integer.parseInt(value, base); + } catch (NumberFormatException e) { + throw new EvalException( + loc, Printer.format("invalid literal for int() with base %d: %r", base, x)); + } + } + + private String getIntegerPrefix(String value) { + value = value.toLowerCase(); + for (String prefix : intPrefixes.keySet()) { + if (value.startsWith(prefix)) { + return prefix; + } + } + return ""; + } + }; @SkylarkSignature(name = "struct", returnType = SkylarkClassObject.class, doc = "Creates an immutable struct using the keyword arguments as attributes. It is used to group "
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 279db38..a9f82d5 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
@@ -1658,8 +1658,8 @@ new BothModesTest() .testStatement("int('1')", 1) .testStatement("int('-1234')", -1234) - .testIfErrorContains("invalid literal for int(): \"1.5\"", "int('1.5')") - .testIfErrorContains("invalid literal for int(): \"ab\"", "int('ab')") + .testIfErrorContains("invalid literal for int() with base 10: \"1.5\"", "int('1.5')") + .testIfErrorContains("invalid literal for int() with base 10: \"ab\"", "int('ab')") .testStatement("int(42)", 42) .testStatement("int(-1)", -1) .testStatement("int(True)", 1) @@ -1668,6 +1668,45 @@ } @Test + public void testIntWithBase() throws Exception { + new BothModesTest() + .testStatement("int('11', 2)", 3) + .testStatement("int('11', 9)", 10) + .testStatement("int('AF', 16)", 175) + .testStatement("int('11', 36)", 37) + .testStatement("int('az', 36)", 395); + } + + @Test + public void testIntWithBase_InvalidBase() throws Exception { + new BothModesTest() + .testIfExactError("invalid literal for int() with base 3: \"123\"", "int('123', 3)") + .testIfExactError("invalid literal for int() with base 15: \"FF\"", "int('FF', 15)") + .testIfExactError("int() base must be >= 2 and <= 36", "int('123', -1)") + .testIfExactError("int() base must be >= 2 and <= 36", "int('123', 1)") + .testIfExactError("int() base must be >= 2 and <= 36", "int('123', 37)"); + } + + @Test + public void testIntWithBase_Prefix() throws Exception { + new BothModesTest() + .testStatement("int('0b11', 0)", 3) + .testStatement("int('0B11', 2)", 3) + .testStatement("int('0o11', 0)", 9) + .testStatement("int('0O11', 8)", 9) + .testStatement("int('0XFF', 0)", 255) + .testStatement("int('0xFF', 16)", 255) + .testIfExactError("invalid literal for int() with base 8: \"0xFF\"", "int('0xFF', 8)"); + } + + @Test + public void testIntWithBase_NoString() throws Exception { + new BothModesTest() + .testIfExactError("int() can't convert non-string with explicit base", "int(True, 2)") + .testIfExactError("int() can't convert non-string with explicit base", "int(1, 2)"); + } + + @Test public void testStrFunction() throws Exception { new SkylarkTest().testStatement("def foo(x): return x\nstr(foo)", "<function foo>"); }