diff options
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/packages/MethodLibrary.java')
-rw-r--r-- | src/main/java/com/google/devtools/build/lib/packages/MethodLibrary.java | 1053 |
1 files changed, 1053 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/packages/MethodLibrary.java b/src/main/java/com/google/devtools/build/lib/packages/MethodLibrary.java new file mode 100644 index 0000000000..5969697337 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/packages/MethodLibrary.java @@ -0,0 +1,1053 @@ +// Copyright 2014 Google Inc. 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.packages; + +import static com.google.devtools.build.lib.syntax.SkylarkFunction.cast; + +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.devtools.build.lib.collect.nestedset.Order; +import com.google.devtools.build.lib.events.Event; +import com.google.devtools.build.lib.events.Location; +import com.google.devtools.build.lib.packages.Type.ConversionException; +import com.google.devtools.build.lib.syntax.AbstractFunction; +import com.google.devtools.build.lib.syntax.AbstractFunction.NoArgFunction; +import com.google.devtools.build.lib.syntax.ClassObject; +import com.google.devtools.build.lib.syntax.ClassObject.SkylarkClassObject; +import com.google.devtools.build.lib.syntax.DotExpression; +import com.google.devtools.build.lib.syntax.Environment; +import com.google.devtools.build.lib.syntax.EvalException; +import com.google.devtools.build.lib.syntax.EvalUtils; +import com.google.devtools.build.lib.syntax.FuncallExpression; +import com.google.devtools.build.lib.syntax.Function; +import com.google.devtools.build.lib.syntax.MixedModeFunction; +import com.google.devtools.build.lib.syntax.SelectorValue; +import com.google.devtools.build.lib.syntax.SkylarkBuiltin; +import com.google.devtools.build.lib.syntax.SkylarkBuiltin.Param; +import com.google.devtools.build.lib.syntax.SkylarkEnvironment; +import com.google.devtools.build.lib.syntax.SkylarkList; +import com.google.devtools.build.lib.syntax.SkylarkModule; +import com.google.devtools.build.lib.syntax.SkylarkNestedSet; +import com.google.devtools.build.lib.syntax.SkylarkType; +import com.google.devtools.build.lib.syntax.SkylarkType.SkylarkFunctionType; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ExecutionException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A helper class containing built in functions for the Build and the Build Extension Language. + */ +public class MethodLibrary { + + private MethodLibrary() {} + + // Convert string index in the same way Python does. + // If index is negative, starts from the end. + // If index is outside bounds, it is restricted to the valid range. + private static int getPythonStringIndex(int index, int stringLength) { + if (index < 0) { + index += stringLength; + } + return Math.max(Math.min(index, stringLength), 0); + } + + // Emulate Python substring function + // It converts out of range indices, and never fails + private static String getPythonSubstring(String str, int start, int end) { + start = getPythonStringIndex(start, str.length()); + end = getPythonStringIndex(end, str.length()); + if (start > end) { + return ""; + } else { + return str.substring(start, end); + } + } + + public static int getListIndex(Object key, int listSize, FuncallExpression ast) + throws ConversionException, EvalException { + // Get the nth element in the list + int index = Type.INTEGER.convert(key, "index operand"); + if (index < 0) { + index += listSize; + } + if (index < 0 || index >= listSize) { + throw new EvalException(ast.getLocation(), "List index out of range (index is " + + index + ", but list has " + listSize + " elements)"); + } + return index; + } + + // supported string methods + + @SkylarkBuiltin(name = "join", objectType = StringModule.class, returnType = String.class, + doc = "Returns a string in which the string elements of the argument have been " + + "joined by this string as a separator. Example:<br>" + + "<pre class=language-python>\"|\".join([\"a\", \"b\", \"c\"]) == \"a|b|c\"</pre>", + mandatoryParams = { + @Param(name = "elements", type = SkylarkList.class, doc = "The objects to join.")}) + private static Function join = new MixedModeFunction("join", + ImmutableList.of("this", "elements"), 2, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) throws ConversionException { + String thiz = Type.STRING.convert(args[0], "'join' operand"); + List<?> seq = Type.OBJECT_LIST.convert(args[1], "'join' argument"); + StringBuilder sb = new StringBuilder(); + for (Iterator<?> i = seq.iterator(); i.hasNext();) { + sb.append(i.next().toString()); + if (i.hasNext()) { + sb.append(thiz); + } + } + return sb.toString(); + } + }; + + @SkylarkBuiltin(name = "lower", objectType = StringModule.class, returnType = String.class, + doc = "Returns the lower case version of this string.") + private static Function lower = new MixedModeFunction("lower", + ImmutableList.of("this"), 1, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) throws ConversionException { + String thiz = Type.STRING.convert(args[0], "'lower' operand"); + return thiz.toLowerCase(); + } + }; + + @SkylarkBuiltin(name = "upper", objectType = StringModule.class, returnType = String.class, + doc = "Returns the upper case version of this string.") + private static Function upper = new MixedModeFunction("upper", + ImmutableList.of("this"), 1, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) throws ConversionException { + String thiz = Type.STRING.convert(args[0], "'upper' operand"); + return thiz.toUpperCase(); + } + }; + + @SkylarkBuiltin(name = "replace", objectType = StringModule.class, returnType = String.class, + doc = "Returns a copy of the string in which the occurrences " + + "of <code>old</code> have been replaced with <code>new</code>, optionally restricting " + + "the number of replacements to <code>maxsplit</code>.", + mandatoryParams = { + @Param(name = "old", type = String.class, doc = "The string to be replaced."), + @Param(name = "new", type = String.class, doc = "The string to replace with.")}, + optionalParams = { + @Param(name = "maxsplit", type = Integer.class, doc = "The maximum number of replacements.")}) + private static Function replace = + new MixedModeFunction("replace", ImmutableList.of("this", "old", "new", "maxsplit"), 3, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) throws EvalException, + ConversionException { + String thiz = Type.STRING.convert(args[0], "'replace' operand"); + String old = Type.STRING.convert(args[1], "'replace' argument"); + String neww = Type.STRING.convert(args[2], "'replace' argument"); + int maxsplit = + args[3] != null ? Type.INTEGER.convert(args[3], "'replace' argument") + : Integer.MAX_VALUE; + StringBuffer sb = new StringBuffer(); + try { + Matcher m = Pattern.compile(old, Pattern.LITERAL).matcher(thiz); + for (int i = 0; i < maxsplit && m.find(); i++) { + m.appendReplacement(sb, Matcher.quoteReplacement(neww)); + } + m.appendTail(sb); + } catch (IllegalStateException e) { + throw new EvalException(ast.getLocation(), e.getMessage() + " in call to replace"); + } + return sb.toString(); + } + }; + + @SkylarkBuiltin(name = "split", objectType = StringModule.class, returnType = SkylarkList.class, + doc = "Returns a list of all the words in the string, using <code>sep</code> " + + "as the separator, optionally limiting the number of splits to <code>maxsplit</code>.", + optionalParams = { + @Param(name = "sep", type = String.class, + doc = "The string to split on, default is space (\" \")."), + @Param(name = "maxsplit", type = Integer.class, doc = "The maximum number of splits.")}) + private static Function split = new MixedModeFunction("split", + ImmutableList.of("this", "sep", "maxsplit"), 1, false) { + @Override + public Object call(Object[] args, FuncallExpression ast, Environment env) + throws ConversionException { + String thiz = Type.STRING.convert(args[0], "'split' operand"); + String sep = args[1] != null + ? Type.STRING.convert(args[1], "'split' argument") + : " "; + int maxsplit = args[2] != null + ? Type.INTEGER.convert(args[2], "'split' argument") + 1 // last is remainder + : -1; + String[] ss = Pattern.compile(sep, Pattern.LITERAL).split(thiz, + maxsplit); + List<String> result = java.util.Arrays.asList(ss); + return env.isSkylarkEnabled() ? SkylarkList.list(result, String.class) : result; + } + }; + + @SkylarkBuiltin(name = "rfind", objectType = StringModule.class, returnType = Integer.class, + doc = "Returns the last index where <code>sub</code> is found, " + + "or -1 if no such index exists, optionally restricting to " + + "[<code>start</code>:<code>end</code>], " + + "<code>start</code> being inclusive and <code>end</code> being exclusive.", + mandatoryParams = { + @Param(name = "sub", type = String.class, doc = "The substring to find.")}, + optionalParams = { + @Param(name = "start", type = Integer.class, doc = "Restrict to search from this position."), + @Param(name = "end", type = Integer.class, doc = "Restrict to search before this position.")}) + private static Function rfind = + new MixedModeFunction("rfind", ImmutableList.of("this", "sub", "start", "end"), 2, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) + throws ConversionException { + String thiz = Type.STRING.convert(args[0], "'rfind' operand"); + String sub = Type.STRING.convert(args[1], "'rfind' argument"); + int start = 0; + if (args[2] != null) { + start = Type.INTEGER.convert(args[2], "'rfind' argument"); + } + int end = thiz.length(); + if (args[3] != null) { + end = Type.INTEGER.convert(args[3], "'rfind' argument"); + } + int subpos = getPythonSubstring(thiz, start, end).lastIndexOf(sub); + start = getPythonStringIndex(start, thiz.length()); + return subpos < 0 ? subpos : subpos + start; + } + }; + + @SkylarkBuiltin(name = "find", objectType = StringModule.class, returnType = Integer.class, + doc = "Returns the first index where <code>sub</code> is found, " + + "or -1 if no such index exists, optionally restricting to " + + "[<code>start</code>:<code>end]</code>, " + + "<code>start</code> being inclusive and <code>end</code> being exclusive.", + mandatoryParams = { + @Param(name = "sub", type = String.class, doc = "The substring to find.")}, + optionalParams = { + @Param(name = "start", type = Integer.class, doc = "Restrict to search from this position."), + @Param(name = "end", type = Integer.class, doc = "Restrict to search before this position.")}) + private static Function find = + new MixedModeFunction("find", ImmutableList.of("this", "sub", "start", "end"), 2, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) + throws ConversionException { + String thiz = Type.STRING.convert(args[0], "'find' operand"); + String sub = Type.STRING.convert(args[1], "'find' argument"); + int start = 0; + if (args[2] != null) { + start = Type.INTEGER.convert(args[2], "'find' argument"); + } + int end = thiz.length(); + if (args[3] != null) { + end = Type.INTEGER.convert(args[3], "'find' argument"); + } + int subpos = getPythonSubstring(thiz, start, end).indexOf(sub); + start = getPythonStringIndex(start, thiz.length()); + return subpos < 0 ? subpos : subpos + start; + } + }; + + @SkylarkBuiltin(name = "count", objectType = StringModule.class, returnType = Integer.class, + doc = "Returns the number of (non-overlapping) occurrences of substring <code>sub</code> in " + + "string, optionally restricting to [<code>start</code>:<code>end</code>], " + + "<code>start</code> being inclusive and <code>end</code> being exclusive.", + mandatoryParams = { + @Param(name = "sub", type = String.class, doc = "The substring to count.")}, + optionalParams = { + @Param(name = "start", type = Integer.class, doc = "Restrict to search from this position."), + @Param(name = "end", type = Integer.class, doc = "Restrict to search before this position.")}) + private static Function count = + new MixedModeFunction("count", ImmutableList.of("this", "sub", "start", "end"), 2, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) + throws ConversionException { + String thiz = Type.STRING.convert(args[0], "'count' operand"); + String sub = Type.STRING.convert(args[1], "'count' argument"); + int start = 0; + if (args[2] != null) { + start = Type.INTEGER.convert(args[2], "'count' argument"); + } + int end = thiz.length(); + if (args[3] != null) { + end = Type.INTEGER.convert(args[3], "'count' argument"); + } + String str = getPythonSubstring(thiz, start, end); + if (sub.equals("")) { + return str.length() + 1; + } + int count = 0; + int index = -1; + while ((index = str.indexOf(sub)) >= 0) { + count++; + str = str.substring(index + sub.length()); + } + return count; + } + }; + + @SkylarkBuiltin(name = "endswith", objectType = StringModule.class, returnType = Boolean.class, + doc = "Returns True if the string ends with <code>sub</code>, " + + "otherwise False, optionally restricting to [<code>start</code>:<code>end</code>], " + + "<code>start</code> being inclusive and <code>end</code> being exclusive.", + mandatoryParams = { + @Param(name = "sub", type = String.class, doc = "The substring to check.")}, + optionalParams = { + @Param(name = "start", type = Integer.class, doc = "Test beginning at this position."), + @Param(name = "end", type = Integer.class, doc = "Stop comparing at this position.")}) + private static Function endswith = + new MixedModeFunction("endswith", ImmutableList.of("this", "sub", "start", "end"), 2, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) + throws ConversionException { + String thiz = Type.STRING.convert(args[0], "'endswith' operand"); + String sub = Type.STRING.convert(args[1], "'endswith' argument"); + int start = 0; + if (args[2] != null) { + start = Type.INTEGER.convert(args[2], "'endswith' argument"); + } + int end = thiz.length(); + if (args[3] != null) { + end = Type.INTEGER.convert(args[3], ""); + } + + return getPythonSubstring(thiz, start, end).endsWith(sub); + } + }; + + @SkylarkBuiltin(name = "startswith", objectType = StringModule.class, returnType = Boolean.class, + doc = "Returns True if the string starts with <code>sub</code>, " + + "otherwise False, optionally restricting to [<code>start</code>:<code>end</code>], " + + "<code>start</code> being inclusive and <code>end</code> being exclusive.", + mandatoryParams = { + @Param(name = "sub", type = String.class, doc = "The substring to check.")}, + optionalParams = { + @Param(name = "start", type = Integer.class, doc = "Test beginning at this position."), + @Param(name = "end", type = Integer.class, doc = "Stop comparing at this position.")}) + private static Function startswith = + new MixedModeFunction("startswith", ImmutableList.of("this", "sub", "start", "end"), 2, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) throws ConversionException { + String thiz = Type.STRING.convert(args[0], "'startswith' operand"); + String sub = Type.STRING.convert(args[1], "'startswith' argument"); + int start = 0; + if (args[2] != null) { + start = Type.INTEGER.convert(args[2], "'startswith' argument"); + } + int end = thiz.length(); + if (args[3] != null) { + end = Type.INTEGER.convert(args[3], "'startswith' argument"); + } + return getPythonSubstring(thiz, start, end).startsWith(sub); + } + }; + + // TODO(bazel-team): Maybe support an argument to tell the type of the whitespace. + @SkylarkBuiltin(name = "strip", objectType = StringModule.class, returnType = String.class, + doc = "Returns a copy of the string in which all whitespace characters " + + "have been stripped from the beginning and the end of the string.") + private static Function strip = + new MixedModeFunction("strip", ImmutableList.of("this"), 1, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) + throws ConversionException { + String operand = Type.STRING.convert(args[0], "'strip' operand"); + return operand.trim(); + } + }; + + // substring operator + @SkylarkBuiltin(name = "$substring", hidden = true, + doc = "String[<code>start</code>:<code>end</code>] returns a substring.") + private static Function substring = new MixedModeFunction("$substring", + ImmutableList.of("this", "start", "end"), 3, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) throws ConversionException { + String thiz = Type.STRING.convert(args[0], "substring operand"); + int left = Type.INTEGER.convert(args[1], "substring operand"); + int right = Type.INTEGER.convert(args[2], "substring operand"); + return getPythonSubstring(thiz, left, right); + } + }; + + // supported list methods + @SkylarkBuiltin(name = "append", hidden = true, + doc = "Adds an item to the end of the list.") + private static Function append = new MixedModeFunction("append", + ImmutableList.of("this", "x"), 2, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) throws EvalException, + ConversionException { + List<Object> thiz = Type.OBJECT_LIST.convert(args[0], "'append' operand"); + thiz.add(args[1]); + return Environment.NONE; + } + }; + + @SkylarkBuiltin(name = "extend", hidden = true, + doc = "Adds all items to the end of the list.") + private static Function extend = new MixedModeFunction("extend", + ImmutableList.of("this", "x"), 2, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) throws EvalException, + ConversionException { + List<Object> thiz = Type.OBJECT_LIST.convert(args[0], "'extend' operand"); + List<Object> l = Type.OBJECT_LIST.convert(args[1], "'extend' argument"); + thiz.addAll(l); + return Environment.NONE; + } + }; + + // dictionary access operator + @SkylarkBuiltin(name = "$index", hidden = true, + doc = "Returns the nth element of a list or string, " + + "or looks up a value in a dictionary.") + private static Function index = new MixedModeFunction("$index", + ImmutableList.of("this", "index"), 2, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) throws EvalException, + ConversionException { + Object collectionCandidate = args[0]; + Object key = args[1]; + + if (collectionCandidate instanceof Map<?, ?>) { + Map<?, ?> dictionary = (Map<?, ?>) collectionCandidate; + if (!dictionary.containsKey(key)) { + throw new EvalException(ast.getLocation(), "Key '" + key + "' not found in dictionary"); + } + return dictionary.get(key); + } else if (collectionCandidate instanceof List<?>) { + + List<Object> list = Type.OBJECT_LIST.convert(collectionCandidate, "index operand"); + + if (!list.isEmpty()) { + int index = getListIndex(key, list.size(), ast); + return list.get(index); + } + + throw new EvalException(ast.getLocation(), "List is empty"); + } else if (collectionCandidate instanceof SkylarkList) { + SkylarkList list = (SkylarkList) collectionCandidate; + + if (!list.isEmpty()) { + int index = getListIndex(key, list.size(), ast); + return list.get(index); + } + + throw new EvalException(ast.getLocation(), "List is empty"); + } else if (collectionCandidate instanceof String) { + String str = (String) collectionCandidate; + int index = getListIndex(key, str.length(), ast); + return str.substring(index, index + 1); + + } else { + // TODO(bazel-team): This is dead code, get rid of it. + throw new EvalException(ast.getLocation(), String.format( + "Unsupported datatype (%s) for indexing, only works for dict and list", + EvalUtils.getDatatypeName(collectionCandidate))); + } + } + }; + + @SkylarkBuiltin(name = "values", objectType = DictModule.class, returnType = SkylarkList.class, + doc = "Return the list of values.") + private static Function values = new NoArgFunction("values") { + @Override + public Object call(Object self, FuncallExpression ast, Environment env) + throws EvalException, InterruptedException { + Map<?, ?> dict = (Map<?, ?>) self; + return convert(dict.values(), env, ast.getLocation()); + } + }; + + @SkylarkBuiltin(name = "items", objectType = DictModule.class, returnType = SkylarkList.class, + doc = "Return the list of key-value tuples.") + private static Function items = new NoArgFunction("items") { + @Override + public Object call(Object self, FuncallExpression ast, Environment env) + throws EvalException, InterruptedException { + Map<?, ?> dict = (Map<?, ?>) self; + List<Object> list = Lists.newArrayListWithCapacity(dict.size()); + for (Map.Entry<?, ?> entries : dict.entrySet()) { + List<?> item = ImmutableList.of(entries.getKey(), entries.getValue()); + list.add(env.isSkylarkEnabled() ? SkylarkList.tuple(item) : item); + } + return convert(list, env, ast.getLocation()); + } + }; + + @SkylarkBuiltin(name = "keys", objectType = DictModule.class, returnType = SkylarkList.class, + doc = "Return the list of keys.") + private static Function keys = new NoArgFunction("keys") { + @Override + public Object call(Object self, FuncallExpression ast, Environment env) + throws EvalException, InterruptedException { + Map<?, ?> dict = (Map<?, ?>) self; + return convert(dict.keySet(), env, ast.getLocation()); + } + }; + + @SuppressWarnings("unchecked") + private static Iterable<Object> convert(Collection<?> list, Environment env, Location loc) + throws EvalException { + if (env.isSkylarkEnabled()) { + return SkylarkList.list(list, loc); + } else { + return Lists.newArrayList(list); + } + } + + // unary minus + @SkylarkBuiltin(name = "-", hidden = true, doc = "Unary minus operator.") + private static Function minus = new MixedModeFunction("-", ImmutableList.of("this"), 1, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) throws ConversionException { + int num = Type.INTEGER.convert(args[0], "'unary minus' argument"); + return -num; + } + }; + + @SkylarkBuiltin(name = "list", returnType = SkylarkList.class, + doc = "Converts a collection (e.g. set or dictionary) to a list.", + mandatoryParams = {@Param(name = "x", doc = "The object to convert.")}) + private static Function list = new MixedModeFunction("list", + ImmutableList.of("list"), 1, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) throws EvalException { + Location loc = ast.getLocation(); + return SkylarkList.list(EvalUtils.toCollection(args[0], loc), loc); + } + }; + + @SkylarkBuiltin(name = "len", returnType = Integer.class, doc = + "Returns the length of a string, list, tuple, set, or dictionary.", + mandatoryParams = {@Param(name = "x", doc = "The object to check length of.")}) + private static Function len = new MixedModeFunction("len", + ImmutableList.of("list"), 1, false) { + + @Override + public Object call(Object[] args, FuncallExpression ast) throws EvalException { + Object arg = args[0]; + int l = EvalUtils.size(arg); + if (l == -1) { + throw new EvalException(ast.getLocation(), + EvalUtils.getDatatypeName(arg) + " is not iterable"); + } + return l; + } + }; + + @SkylarkBuiltin(name = "str", returnType = String.class, doc = + "Converts any object to string. This is useful for debugging.", + mandatoryParams = {@Param(name = "x", doc = "The object to convert.")}) + private static Function str = new MixedModeFunction("str", ImmutableList.of("this"), 1, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) throws EvalException { + return EvalUtils.printValue(args[0]); + } + }; + + @SkylarkBuiltin(name = "bool", returnType = Boolean.class, doc = "Converts an object to boolean. " + + "It returns False if the object is None, False, an empty string, the number 0, or an " + + "empty collection. Otherwise, it returns True.", + mandatoryParams = {@Param(name = "x", doc = "The variable to convert.")}) + private static Function bool = new MixedModeFunction("bool", + ImmutableList.of("this"), 1, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) throws EvalException { + return EvalUtils.toBoolean(args[0]); + } + }; + + @SkylarkBuiltin(name = "struct", returnType = SkylarkClassObject.class, doc = + "Creates an immutable struct using the keyword arguments as fields. It is used to group " + + "multiple values together.Example:<br>" + + "<pre class=language-python>s = struct(x = 2, y = 3)\n" + + "return s.x + s.y # returns 5</pre>") + private static Function struct = new AbstractFunction("struct") { + + @Override + public Object call(List<Object> args, Map<String, Object> kwargs, FuncallExpression ast, + Environment env) throws EvalException, InterruptedException { + if (args.size() > 0) { + throw new EvalException(ast.getLocation(), "struct only supports keyword arguments"); + } + return new SkylarkClassObject(kwargs, ast.getLocation()); + } + }; + + @SkylarkBuiltin(name = "set", returnType = SkylarkNestedSet.class, + doc = "Creates a set from the <code>items</code>, that supports nesting. " + + "The nesting is applied to other nested sets among <code>items</code>.<br>" + + "Examples:<br>" + + "<pre class=language-python>set([1, set([2, 3]), 2])\n" + + "set([1, 2, 3], order=\"compile\")</pre>", + optionalParams = { + @Param(name = "items", type = SkylarkList.class, + doc = "The items to initialize the set with."), + @Param(name = "order", type = String.class, + doc = "The ordering strategy for the set if it's nested, " + + "possible values are: <code>stable</code> (default), <code>compile</code>, " + + "<code>link</code> or <code>naive_link</code>.")}) + private static final Function set = + new MixedModeFunction("set", ImmutableList.of("items", "order"), 0, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) throws EvalException, + ConversionException { + Order order; + if (args[1] == null || args[1].equals("stable")) { + order = Order.STABLE_ORDER; + } else if (args[1].equals("compile")) { + order = Order.COMPILE_ORDER; + } else if (args[1].equals("link")) { + order = Order.LINK_ORDER; + } else if (args[1].equals("naive_link")) { + order = Order.NAIVE_LINK_ORDER; + } else { + throw new EvalException(ast.getLocation(), "Invalid order: " + args[1]); + } + + if (args[0] == null) { + return new SkylarkNestedSet(order, SkylarkList.EMPTY_LIST, ast.getLocation()); + } + return new SkylarkNestedSet(order, args[0], ast.getLocation()); + } + }; + + @SkylarkBuiltin(name = "enumerate", returnType = SkylarkList.class, + doc = "Return a list of pairs, with the index (int) and the item from the input list.\n" + + "<pre class=language-python>" + + "enumerate([24, 21, 84]) == [[0, 24], [1, 21], [2, 84]]</pre>\n", + mandatoryParams = { + @Param(name = "list", type = SkylarkList.class, + doc = "input list"), + }) + private static Function enumerate = new MixedModeFunction("enumerate", + ImmutableList.of("list"), 1, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) throws EvalException, + ConversionException { + List<Object> input = Type.OBJECT_LIST.convert(args[0], "'enumerate' operand"); + List<List<Object>> result = Lists.newArrayList(); + int count = 0; + for (Object obj : input) { + result.add(Lists.newArrayList(count, obj)); + count++; + } + return result; + } + }; + + @SkylarkBuiltin(name = "range", returnType = SkylarkList.class, + doc = "Creates a list where items go from <code>start</code> to <end>, using a " + + "<code>step</code> increment. If a single argument is provided, items will " + + "range from 0 to that element." + + "<pre class=language-python>range(4) == [0, 1, 2, 3]\n" + + "range(3, 9, 2) == [3, 5, 7]\n" + + "range(3, 0, -1) == [3, 2, 1]</pre>", + mandatoryParams = { + @Param(name = "start", type = Integer.class, + doc = "Value of the first element"), + }, + optionalParams = { + @Param(name = "end", type = SkylarkList.class, + doc = "Generation of the list stops before <code>end</code> is reached."), + @Param(name = "step", type = String.class, + doc = "The increment (default is 1). It may be negative.")}) + private static final Function range = + new MixedModeFunction("range", ImmutableList.of("start", "stop", "step"), 1, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) throws EvalException, + ConversionException { + int start; + int stop; + if (args[1] == null) { + start = 0; + stop = Type.INTEGER.convert(args[0], "stop"); + } else { + start = Type.INTEGER.convert(args[0], "start"); + stop = Type.INTEGER.convert(args[1], "stop"); + } + int step = args[2] == null ? 1 : Type.INTEGER.convert(args[2], "step"); + if (step == 0) { + throw new EvalException(ast.getLocation(), "step cannot be 0"); + } + List<Integer> result = Lists.newArrayList(); + if (step > 0) { + while (start < stop) { + result.add(start); + start += step; + } + } else { + while (start > stop) { + result.add(start); + start += step; + } + } + return SkylarkList.list(result, Integer.class); + } + }; + + /** + * Returns a function-value implementing "select" (i.e. configurable attributes) + * in the specified package context. + */ + @SkylarkBuiltin(name = "select", + doc = "Creates a SelectorValue from the dict parameter.", + mandatoryParams = {@Param(name = "x", type = Map.class, doc = "The parameter to convert.")}) + private static final Function select = new MixedModeFunction("select", + ImmutableList.of("x"), 1, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) + throws EvalException, ConversionException { + Object dict = args[0]; + if (!(dict instanceof Map<?, ?>)) { + throw new EvalException(ast.getLocation(), + "select({...}) argument isn't a dictionary"); + } + return new SelectorValue((Map<?, ?>) dict); + } + }; + + /** + * Returns true if the object has a field of the given name, otherwise false. + */ + @SkylarkBuiltin(name = "hasattr", returnType = Boolean.class, + doc = "Returns True if the object <code>x</code> has a field of the given <code>name</code>, " + + "otherwise False. Example:<br>" + + "<pre class=language-python>hasattr(ctx.attr, \"myattr\")</pre>", + mandatoryParams = { + @Param(name = "object", doc = "The object to check."), + @Param(name = "name", type = String.class, doc = "The name of the field.")}) + private static final Function hasattr = + new MixedModeFunction("hasattr", ImmutableList.of("object", "name"), 2, false) { + + @Override + public Object call(Object[] args, FuncallExpression ast, Environment env) + throws EvalException, ConversionException { + Object obj = args[0]; + String name = cast(args[1], String.class, "name", ast.getLocation()); + + if (obj instanceof ClassObject && ((ClassObject) obj).getValue(name) != null) { + return true; + } + + if (env.getFunctionNames(obj.getClass()).contains(name)) { + return true; + } + + try { + return FuncallExpression.getMethodNames(obj.getClass()).contains(name); + } catch (ExecutionException e) { + // This shouldn't happen + throw new EvalException(ast.getLocation(), e.getMessage()); + } + } + }; + + @SkylarkBuiltin(name = "getattr", + doc = "Returns the struct's field of the given name if exists, otherwise <code>default</code>" + + " if specified, otherwise rasies an error. For example, <code>getattr(x, \"foobar\")" + + "</code> is equivalent to <code>x.foobar</code>." + + "Example:<br>" + + "<pre class=language-python>getattr(ctx.attr, \"myattr\")\n" + + "getattr(ctx.attr, \"myattr\", \"mydefault\")</pre>", + mandatoryParams = { + @Param(name = "object", doc = "The struct which's field is accessed."), + @Param(name = "name", doc = "The name of the struct field.")}, + optionalParams = { + @Param(name = "default", doc = "The default value to return in case the struct " + + "doesn't have a field of the given name.")}) + private static final Function getattr = new MixedModeFunction( + "getattr", ImmutableList.of("object", "name", "default"), 2, false) { + @Override + public Object call(Object[] args, FuncallExpression ast, Environment env) + throws EvalException { + Object obj = args[0]; + String name = cast(args[1], String.class, "name", ast.getLocation()); + Object result = DotExpression.eval(obj, name, ast.getLocation()); + if (result == null) { + if (args[2] != null) { + return args[2]; + } else { + throw new EvalException(ast.getLocation(), "Object of type '" + + EvalUtils.getDatatypeName(obj) + "' has no field '" + name + "'"); + } + } + return result; + } + }; + + @SkylarkBuiltin(name = "dir", returnType = SkylarkList.class, + doc = "Returns the list of the names (list of strings) of the fields and " + + "methods of the parameter object.", + mandatoryParams = {@Param(name = "object", doc = "The object to check.")}) + private static final Function dir = new MixedModeFunction( + "dir", ImmutableList.of("object"), 1, false) { + @Override + public Object call(Object[] args, FuncallExpression ast, Environment env) + throws EvalException { + Object obj = args[0]; + // Order the fields alphabetically. + Set<String> fields = new TreeSet<>(); + if (obj instanceof ClassObject) { + fields.addAll(((ClassObject) obj).getKeys()); + } + fields.addAll(env.getFunctionNames(obj.getClass())); + try { + fields.addAll(FuncallExpression.getMethodNames(obj.getClass())); + } catch (ExecutionException e) { + // This shouldn't happen + throw new EvalException(ast.getLocation(), e.getMessage()); + } + return SkylarkList.list(fields, String.class); + } + }; + + @SkylarkBuiltin(name = "type", returnType = String.class, + doc = "Returns the type name of its argument.", + mandatoryParams = {@Param(name = "object", doc = "The object to check type of.")}) + private static final Function type = new MixedModeFunction("type", + ImmutableList.of("object"), 1, false) { + @Override + public Object call(Object[] args, FuncallExpression ast) throws EvalException { + // There is no 'type' type in Skylark, so we return a string with the type name. + return EvalUtils.getDatatypeName(args[0]); + } + }; + + @SkylarkBuiltin(name = "fail", + doc = "Raises an error (the execution stops), except if the <code>when</code> condition " + + "is False.", + returnType = Environment.NoneType.class, + mandatoryParams = { + @Param(name = "msg", type = String.class, doc = "Error message to display for the user")}, + optionalParams = { + @Param(name = "attr", type = String.class, + doc = "The name of the attribute that caused the error"), + @Param(name = "when", type = Boolean.class, + doc = "When False, the function does nothing. Default is True.")}) + private static final Function fail = new MixedModeFunction( + "fail", ImmutableList.of("msg", "attr", "when"), 1, false) { + @Override + public Object call(Object[] args, FuncallExpression ast, Environment env) + throws EvalException { + if (args[2] != null) { + if (!EvalUtils.toBoolean(args[2])) { + return Environment.NONE; + } + } + String msg = cast(args[0], String.class, "msg", ast.getLocation()); + if (args[1] != null) { + msg = "attribute " + cast(args[1], String.class, "attr", ast.getLocation()) + + ": " + msg; + } + throw new EvalException(ast.getLocation(), msg); + } + }; + + @SkylarkBuiltin(name = "print", returnType = Environment.NoneType.class, + doc = "Prints <code>msg</code> to the console.", + mandatoryParams = { + @Param(name = "*args", doc = "The objects to print.")}, + optionalParams = { + @Param(name = "sep", type = String.class, + doc = "The separator string between the objects, default is space (\" \").")}) + private static final Function print = new AbstractFunction("print") { + @Override + public Object call(List<Object> args, Map<String, Object> kwargs, FuncallExpression ast, + Environment env) throws EvalException, InterruptedException { + String sep = " "; + if (kwargs.containsKey("sep")) { + sep = cast(kwargs.remove("sep"), String.class, "sep", ast.getLocation()); + } + if (kwargs.size() > 0) { + throw new EvalException(ast.getLocation(), + "unexpected keywords: '" + kwargs.keySet() + "'"); + } + String msg = Joiner.on(sep).join(Iterables.transform(args, + new com.google.common.base.Function<Object, String>() { + @Override + public String apply(Object input) { + return EvalUtils.printValue(input); + } + })); + ((SkylarkEnvironment) env).handleEvent(Event.warn(ast.getLocation(), msg)); + return Environment.NONE; + } + }; + + /** + * Skylark String module. + */ + @SkylarkModule(name = "string", doc = + "A language built-in type to support strings. " + + "Example of string literals:<br>" + + "<pre class=language-python>a = 'abc\\ndef'\n" + + "b = \"ab'cd\"\n" + + "c = \"\"\"multiline string\"\"\"</pre>" + + "Strings are iterable and support the <code>in</code> operator. Examples:<br>" + + "<pre class=language-python>\"a\" in \"abc\" # evaluates as True\n" + + "l = []\n" + + "for s in \"abc\":\n" + + " l += [s] # l == [\"a\", \"b\", \"c\"]</pre>") + public static final class StringModule {} + + /** + * Skylark Dict module. + */ + @SkylarkModule(name = "dict", doc = + "A language built-in type to support dicts. " + + "Example of dict literal:<br>" + + "<pre class=language-python>d = {\"a\": 2, \"b\": 5}</pre>" + + "Accessing elements works just like in Python:<br>" + + "<pre class=language-python>e = d[\"a\"] # e == 2</pre>" + + "Dicts support the <code>+</code> operator to concatenate two dicts. In case of multiple " + + "keys the second one overrides the first one. Examples:<br>" + + "<pre class=language-python>" + + "d = {\"a\" : 1} + {\"b\" : 2} # d == {\"a\" : 1, \"b\" : 2}\n" + + "d += {\"c\" : 3} # d == {\"a\" : 1, \"b\" : 2, \"c\" : 3}\n" + + "d = d + {\"c\" : 5} # d == {\"a\" : 1, \"b\" : 2, \"c\" : 5}</pre>" + + "Since the language doesn't have mutable objects <code>d[\"a\"] = 5</code> automatically " + + "translates to <code>d = d + {\"a\" : 5}</code>.<br>" + + "Dicts are iterable, the iteration works on their keyset.<br>" + + "Dicts support the <code>in</code> operator, testing membership in the keyset of the dict. " + + "Example:<br>" + + "<pre class=language-python>\"a\" in {\"a\" : 2, \"b\" : 5} # evaluates as True</pre>") + public static final class DictModule {} + + public static final Map<Function, SkylarkType> stringFunctions = ImmutableMap + .<Function, SkylarkType>builder() + .put(join, SkylarkType.STRING) + .put(lower, SkylarkType.STRING) + .put(upper, SkylarkType.STRING) + .put(replace, SkylarkType.STRING) + .put(split, SkylarkType.of(List.class, String.class)) + .put(rfind, SkylarkType.INT) + .put(find, SkylarkType.INT) + .put(endswith, SkylarkType.BOOL) + .put(startswith, SkylarkType.BOOL) + .put(strip, SkylarkType.STRING) + .put(substring, SkylarkType.STRING) + .put(count, SkylarkType.INT) + .build(); + + public static final List<Function> listFunctions = ImmutableList + .<Function>builder() + .add(append) + .add(extend) + .build(); + + public static final Map<Function, SkylarkType> dictFunctions = ImmutableMap + .<Function, SkylarkType>builder() + .put(items, SkylarkType.of(List.class)) + .put(keys, SkylarkType.of(Set.class)) + .put(values, SkylarkType.of(List.class)) + .build(); + + private static final Map<Function, SkylarkType> pureGlobalFunctions = ImmutableMap + .<Function, SkylarkType>builder() + // TODO(bazel-team): String methods are added two times, because there are + // a lot of cases when they are used as global functions in the depot. Those + // should be cleaned up first. + .put(minus, SkylarkType.INT) + .put(select, SkylarkType.of(SelectorValue.class)) + .put(len, SkylarkType.INT) + .put(str, SkylarkType.STRING) + .put(bool, SkylarkType.BOOL) + .build(); + + private static final Map<Function, SkylarkType> skylarkGlobalFunctions = ImmutableMap + .<Function, SkylarkType>builder() + .putAll(pureGlobalFunctions) + .put(list, SkylarkType.of(SkylarkList.class)) + .put(struct, SkylarkType.of(ClassObject.class)) + .put(hasattr, SkylarkType.BOOL) + .put(getattr, SkylarkType.UNKNOWN) + .put(set, SkylarkType.of(SkylarkNestedSet.class)) + .put(dir, SkylarkType.of(SkylarkList.class, String.class)) + .put(enumerate, SkylarkType.of(SkylarkList.class)) + .put(range, SkylarkType.of(SkylarkList.class, Integer.class)) + .put(type, SkylarkType.of(String.class)) + .put(fail, SkylarkType.NONE) + .put(print, SkylarkType.NONE) + .build(); + + /** + * Set up a given environment for supported class methods. + */ + public static void setupMethodEnvironment(Environment env) { + env.registerFunction(Map.class, index.getName(), index); + setupMethodEnvironment(env, Map.class, dictFunctions.keySet()); + env.registerFunction(String.class, index.getName(), index); + setupMethodEnvironment(env, String.class, stringFunctions.keySet()); + if (env.isSkylarkEnabled()) { + env.registerFunction(SkylarkList.class, index.getName(), index); + setupMethodEnvironment(env, skylarkGlobalFunctions.keySet()); + } else { + env.registerFunction(List.class, index.getName(), index); + env.registerFunction(ImmutableList.class, index.getName(), index); + // TODO(bazel-team): listFunctions are not allowed in Skylark extensions (use += instead). + // It is allowed in BUILD files only for backward-compatibility. + setupMethodEnvironment(env, List.class, listFunctions); + setupMethodEnvironment(env, stringFunctions.keySet()); + setupMethodEnvironment(env, pureGlobalFunctions.keySet()); + } + } + + private static void setupMethodEnvironment( + Environment env, Class<?> nameSpace, Iterable<Function> functions) { + for (Function function : functions) { + env.registerFunction(nameSpace, function.getName(), function); + } + } + + private static void setupMethodEnvironment(Environment env, Iterable<Function> functions) { + for (Function function : functions) { + env.update(function.getName(), function); + } + } + + private static void setupValidationEnvironment( + Map<Function, SkylarkType> functions, Map<String, SkylarkType> result) { + for (Map.Entry<Function, SkylarkType> function : functions.entrySet()) { + String name = function.getKey().getName(); + result.put(name, SkylarkFunctionType.of(name, function.getValue())); + } + } + + public static void setupValidationEnvironment( + Map<SkylarkType, Map<String, SkylarkType>> builtIn) { + Map<String, SkylarkType> global = builtIn.get(SkylarkType.GLOBAL); + setupValidationEnvironment(skylarkGlobalFunctions, global); + + Map<String, SkylarkType> dict = new HashMap<>(); + setupValidationEnvironment(dictFunctions, dict); + builtIn.put(SkylarkType.of(Map.class), dict); + + Map<String, SkylarkType> string = new HashMap<>(); + setupValidationEnvironment(stringFunctions, string); + builtIn.put(SkylarkType.STRING, string); + } +} |