// 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.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
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.SelectorList;
import com.google.devtools.build.lib.syntax.SelectorValue;
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.SkylarkSignature;
import com.google.devtools.build.lib.syntax.SkylarkSignature.Param;
import com.google.devtools.build.lib.syntax.SkylarkSignatureProcessor.HackHackEitherList;
import java.util.ArrayList;
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.TreeMap;
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() {}
// TODO(bazel-team):
// the Build language and Skylark currently have different list types:
// the Build language uses plain java List (usually ArrayList) which is mutable and accepts
// any argument, whereas Skylark uses SkylarkList which is immutable and accepts only
// arguments of the same kind. Some methods below use HackHackEitherList as a magic marker
// to indicate that either kind of lists is used depending on the evaluation context.
// It might be a good idea to either have separate methods for the two languages where it matters,
// or to unify the two languages so they use the same data structure (which might require
// updating all existing clients).
// 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 clampIndex(int index, int length) {
if (index < 0) {
index += length;
}
return Math.max(Math.min(index, length), 0);
}
// Emulate Python substring function
// It converts out of range indices, and never fails
private static String pythonSubstring(String str, int start, Object end, String msg)
throws ConversionException {
if (start == 0 && EvalUtils.isNullOrNone(end)) {
return str;
}
start = clampIndex(start, str.length());
int stop;
if (EvalUtils.isNullOrNone(end)) {
stop = str.length();
} else {
stop = clampIndex(Type.INTEGER.convert(end, msg), str.length());
}
if (start >= stop) {
return "";
}
return str.substring(start, stop);
}
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
@SkylarkSignature(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: "
+ "
\"|\".join([\"a\", \"b\", \"c\"]) == \"a|b|c\"
",
mandatoryPositionals = {
@Param(name = "self", type = String.class, doc = "This string, a separator."),
@Param(name = "elements", type = HackHackEitherList.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 thisString = Type.STRING.convert(args[0], "'join' operand");
List> seq = Type.OBJECT_LIST.convert(args[1], "'join' argument");
return Joiner.on(thisString).join(seq);
}};
@SkylarkSignature(name = "lower", objectType = StringModule.class, returnType = String.class,
doc = "Returns the lower case version of this string.",
mandatoryPositionals = {
@Param(name = "self", type = String.class, doc = "This string, to convert to lower case.")})
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();
}
};
@SkylarkSignature(name = "upper", objectType = StringModule.class, returnType = String.class,
doc = "Returns the upper case version of this string.",
mandatoryPositionals = {
@Param(name = "self", type = String.class, doc = "This string, to convert to upper case.")})
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();
}
};
@SkylarkSignature(name = "replace", objectType = StringModule.class, returnType = String.class,
doc = "Returns a copy of the string in which the occurrences "
+ "of old have been replaced with new, optionally restricting "
+ "the number of replacements to maxsplit.",
mandatoryPositionals = {
@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.")},
optionalPositionals = {
@Param(name = "maxsplit", type = Integer.class, noneable = true, defaultValue = "None",
doc = "The maximum number of replacements.")},
useLocation = true)
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();
}
};
@SkylarkSignature(name = "split", objectType = StringModule.class,
returnType = HackHackEitherList.class,
doc = "Returns a list of all the words in the string, using sep "
+ "as the separator, optionally limiting the number of splits to maxsplit.",
mandatoryPositionals = {
@Param(name = "self", type = String.class, doc = "This string.")},
optionalPositionals = {
@Param(name = "sep", type = String.class, defaultValue = "' '",
doc = "The string to split on, default is space (\" \")."),
@Param(name = "maxsplit", type = Integer.class, noneable = true, defaultValue = "None",
doc = "The maximum number of splits.")},
useEnvironment = true)
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 result = java.util.Arrays.asList(ss);
return env.isSkylarkEnabled() ? SkylarkList.list(result, String.class) : result;
}
};
/**
* Common implementation for find, rfind, index, rindex.
* @param forward true if we want to return the last matching index.
*/
private static int stringFind(boolean forward,
String self, String sub, int start, Object end, String msg)
throws ConversionException {
String substr = pythonSubstring(self, start, end, msg);
int subpos = forward ? substr.indexOf(sub) : substr.lastIndexOf(sub);
start = clampIndex(start, self.length());
return subpos < 0 ? subpos : subpos + start;
}
// Common implementation for find, rfind, index, rindex.
// forward is true iff we want to return the last matching index.
private static int stringFind(String functionName, boolean forward, Object[] args)
throws ConversionException {
String thiz = Type.STRING.convert(args[0], functionName + " operand");
String sub = Type.STRING.convert(args[1], functionName + " argument");
int start = 0;
if (!EvalUtils.isNullOrNone(args[2])) {
start = Type.INTEGER.convert(args[2], functionName + " argument");
}
return stringFind(forward, thiz, sub, start, args[3], functionName + " argument");
}
@SkylarkSignature(name = "rfind", objectType = StringModule.class, returnType = Integer.class,
doc = "Returns the last index where sub is found, "
+ "or -1 if no such index exists, optionally restricting to "
+ "[start:end], "
+ "start being inclusive and end being exclusive.",
mandatoryPositionals = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "sub", type = String.class, doc = "The substring to find.")},
optionalPositionals = {
@Param(name = "start", type = Integer.class, defaultValue = "0",
doc = "Restrict to search from this position."),
@Param(name = "end", type = Integer.class, noneable = true, defaultValue = "None",
doc = "optional position before which to restrict to search.")})
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 {
return stringFind("rfind", false, args);
}
};
@SkylarkSignature(name = "find", objectType = StringModule.class, returnType = Integer.class,
doc = "Returns the first index where sub is found, "
+ "or -1 if no such index exists, optionally restricting to "
+ "[start:end], "
+ "start being inclusive and end being exclusive.",
mandatoryPositionals = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "sub", type = String.class, doc = "The substring to find.")},
optionalPositionals = {
@Param(name = "start", type = Integer.class, defaultValue = "0",
doc = "Restrict to search from this position."),
@Param(name = "end", type = Integer.class, noneable = true, defaultValue = "None",
doc = "optional position before which to restrict to search.")})
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 {
return stringFind("find", true, args);
}
};
@SkylarkSignature(name = "rindex", objectType = StringModule.class, returnType = Integer.class,
doc = "Returns the last index where sub is found, "
+ "or throw an error if no such index exists, optionally restricting to "
+ "[start:end], "
+ "start being inclusive and end being exclusive.",
mandatoryPositionals = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "sub", type = String.class, doc = "The substring to find.")},
optionalPositionals = {
@Param(name = "start", type = Integer.class, defaultValue = "0",
doc = "Restrict to search from this position."),
@Param(name = "end", type = Integer.class, noneable = true, defaultValue = "None",
doc = "optional position before which to restrict to search.")},
useLocation = true)
private static Function rindex =
new MixedModeFunction("rindex", ImmutableList.of("this", "sub", "start", "end"), 2, false) {
@Override
public Object call(Object[] args, FuncallExpression ast)
throws EvalException, ConversionException {
int res = stringFind("rindex", false, args);
if (res < 0) {
throw new EvalException(ast.getLocation(),
"substring " + EvalUtils.prettyPrintValue(args[1])
+ " not found in " + EvalUtils.prettyPrintValue(args[0]));
}
return res;
}
};
@SkylarkSignature(name = "index", objectType = StringModule.class, returnType = Integer.class,
doc = "Returns the first index where sub is found, "
+ "or throw an error if no such index exists, optionally restricting to "
+ "[start:end], "
+ "start being inclusive and end being exclusive.",
mandatoryPositionals = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "sub", type = String.class, doc = "The substring to find.")},
optionalPositionals = {
@Param(name = "start", type = Integer.class, defaultValue = "0",
doc = "Restrict to search from this position."),
@Param(name = "end", type = Integer.class, noneable = true,
doc = "optional position before which to restrict to search.")},
useLocation = true)
private static Function index =
new MixedModeFunction("index", ImmutableList.of("this", "sub", "start", "end"), 2, false) {
@Override
public Object call(Object[] args, FuncallExpression ast)
throws EvalException, ConversionException {
int res = stringFind("index", true, args);
if (res < 0) {
throw new EvalException(ast.getLocation(),
"substring " + EvalUtils.prettyPrintValue(args[1])
+ " not found in " + EvalUtils.prettyPrintValue(args[0]));
}
return res;
}
};
@SkylarkSignature(name = "count", objectType = StringModule.class, returnType = Integer.class,
doc = "Returns the number of (non-overlapping) occurrences of substring sub in "
+ "string, optionally restricting to [start:end], "
+ "start being inclusive and end being exclusive.",
mandatoryPositionals = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "sub", type = String.class, doc = "The substring to count.")},
optionalPositionals = {
@Param(name = "start", type = Integer.class, defaultValue = "0",
doc = "Restrict to search from this position."),
@Param(name = "end", type = Integer.class, noneable = true, defaultValue = "None",
doc = "optional position before which to restrict to search.")})
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");
}
String str = pythonSubstring(thiz, start, args[3], "'end' argument to 'count'");
if (sub.isEmpty()) {
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;
}
};
@SkylarkSignature(name = "endswith", objectType = StringModule.class, returnType = Boolean.class,
doc = "Returns True if the string ends with sub, "
+ "otherwise False, optionally restricting to [start:end], "
+ "start being inclusive and end being exclusive.",
mandatoryPositionals = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "sub", type = String.class, doc = "The substring to check.")},
optionalPositionals = {
@Param(name = "start", type = Integer.class, defaultValue = "0",
doc = "Test beginning at this position."),
@Param(name = "end", type = Integer.class, noneable = true, defaultValue = "None",
doc = "optional position at which to stop comparing.")})
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");
}
return pythonSubstring(thiz, start, args[3], "'end' argument to 'endswith'")
.endsWith(sub);
}
};
@SkylarkSignature(name = "startswith", objectType = StringModule.class,
returnType = Boolean.class,
doc = "Returns True if the string starts with sub, "
+ "otherwise False, optionally restricting to [start:end], "
+ "start being inclusive and end being exclusive.",
mandatoryPositionals = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "sub", type = String.class, doc = "The substring to check.")},
optionalPositionals = {
@Param(name = "start", type = Integer.class, defaultValue = "0",
doc = "Test beginning at this position."),
@Param(name = "end", type = Integer.class, noneable = true, defaultValue = "None",
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");
}
return pythonSubstring(thiz, start, args[3], "'end' argument to 'startswith'")
.startsWith(sub);
}
};
// TODO(bazel-team): Maybe support an argument to tell the type of the whitespace.
@SkylarkSignature(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.",
mandatoryPositionals = {
@Param(name = "self", type = String.class, doc = "This 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();
}
};
// slice operator
@SkylarkSignature(name = "$slice", documented = false,
mandatoryPositionals = {
@Param(name = "self", type = Object.class, doc = "This string, list or tuple."),
@Param(name = "start", type = Integer.class, doc = "start position of the slice."),
@Param(name = "end", type = Integer.class, doc = "end position of the slice.")},
doc = "x[start:end] returns a slice or a list slice.",
useLocation = true, useEnvironment = true)
private static Function slice = new MixedModeFunction("$slice",
ImmutableList.of("this", "start", "end"), 3, false) {
@Override
public Object call(Object[] args, FuncallExpression ast, Environment env)
throws EvalException, ConversionException {
int left = Type.INTEGER.convert(args[1], "start operand");
int right = Type.INTEGER.convert(args[2], "end operand");
// Substring
if (args[0] instanceof String) {
String thiz = Type.STRING.convert(args[0], "substring operand");
return pythonSubstring(thiz, left, right, "");
}
// List slice
List