blob: 4430ba7f87d5165a8cdbe3465450236ca5ca2d81 [file] [log] [blame]
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +01001// Copyright 2014 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.devtools.build.lib.packages;
16
17import static com.google.devtools.build.lib.syntax.SkylarkFunction.cast;
18
19import com.google.common.base.Joiner;
20import com.google.common.collect.ImmutableList;
21import com.google.common.collect.ImmutableMap;
22import com.google.common.collect.Iterables;
23import com.google.common.collect.Lists;
24import com.google.devtools.build.lib.collect.nestedset.Order;
25import com.google.devtools.build.lib.events.Event;
26import com.google.devtools.build.lib.events.Location;
27import com.google.devtools.build.lib.packages.Type.ConversionException;
28import com.google.devtools.build.lib.syntax.AbstractFunction;
29import com.google.devtools.build.lib.syntax.AbstractFunction.NoArgFunction;
30import com.google.devtools.build.lib.syntax.ClassObject;
31import com.google.devtools.build.lib.syntax.ClassObject.SkylarkClassObject;
32import com.google.devtools.build.lib.syntax.DotExpression;
33import com.google.devtools.build.lib.syntax.Environment;
34import com.google.devtools.build.lib.syntax.EvalException;
35import com.google.devtools.build.lib.syntax.EvalUtils;
36import com.google.devtools.build.lib.syntax.FuncallExpression;
37import com.google.devtools.build.lib.syntax.Function;
38import com.google.devtools.build.lib.syntax.MixedModeFunction;
39import com.google.devtools.build.lib.syntax.SelectorValue;
40import com.google.devtools.build.lib.syntax.SkylarkBuiltin;
41import com.google.devtools.build.lib.syntax.SkylarkBuiltin.Param;
42import com.google.devtools.build.lib.syntax.SkylarkEnvironment;
43import com.google.devtools.build.lib.syntax.SkylarkList;
44import com.google.devtools.build.lib.syntax.SkylarkModule;
45import com.google.devtools.build.lib.syntax.SkylarkNestedSet;
46import com.google.devtools.build.lib.syntax.SkylarkType;
47import com.google.devtools.build.lib.syntax.SkylarkType.SkylarkFunctionType;
48
49import java.util.Collection;
50import java.util.HashMap;
51import java.util.Iterator;
52import java.util.List;
53import java.util.Map;
54import java.util.Set;
55import java.util.TreeSet;
56import java.util.concurrent.ExecutionException;
57import java.util.regex.Matcher;
58import java.util.regex.Pattern;
59
60/**
61 * A helper class containing built in functions for the Build and the Build Extension Language.
62 */
63public class MethodLibrary {
64
65 private MethodLibrary() {}
66
67 // Convert string index in the same way Python does.
68 // If index is negative, starts from the end.
69 // If index is outside bounds, it is restricted to the valid range.
70 private static int getPythonStringIndex(int index, int stringLength) {
71 if (index < 0) {
72 index += stringLength;
73 }
74 return Math.max(Math.min(index, stringLength), 0);
75 }
76
77 // Emulate Python substring function
78 // It converts out of range indices, and never fails
79 private static String getPythonSubstring(String str, int start, int end) {
80 start = getPythonStringIndex(start, str.length());
81 end = getPythonStringIndex(end, str.length());
82 if (start > end) {
83 return "";
84 } else {
85 return str.substring(start, end);
86 }
87 }
88
89 public static int getListIndex(Object key, int listSize, FuncallExpression ast)
90 throws ConversionException, EvalException {
91 // Get the nth element in the list
92 int index = Type.INTEGER.convert(key, "index operand");
93 if (index < 0) {
94 index += listSize;
95 }
96 if (index < 0 || index >= listSize) {
97 throw new EvalException(ast.getLocation(), "List index out of range (index is "
98 + index + ", but list has " + listSize + " elements)");
99 }
100 return index;
101 }
102
103 // supported string methods
104
105 @SkylarkBuiltin(name = "join", objectType = StringModule.class, returnType = String.class,
106 doc = "Returns a string in which the string elements of the argument have been "
107 + "joined by this string as a separator. Example:<br>"
108 + "<pre class=language-python>\"|\".join([\"a\", \"b\", \"c\"]) == \"a|b|c\"</pre>",
109 mandatoryParams = {
110 @Param(name = "elements", type = SkylarkList.class, doc = "The objects to join.")})
111 private static Function join = new MixedModeFunction("join",
112 ImmutableList.of("this", "elements"), 2, false) {
113 @Override
114 public Object call(Object[] args, FuncallExpression ast) throws ConversionException {
115 String thiz = Type.STRING.convert(args[0], "'join' operand");
116 List<?> seq = Type.OBJECT_LIST.convert(args[1], "'join' argument");
117 StringBuilder sb = new StringBuilder();
118 for (Iterator<?> i = seq.iterator(); i.hasNext();) {
119 sb.append(i.next().toString());
120 if (i.hasNext()) {
121 sb.append(thiz);
122 }
123 }
124 return sb.toString();
125 }
126 };
127
128 @SkylarkBuiltin(name = "lower", objectType = StringModule.class, returnType = String.class,
129 doc = "Returns the lower case version of this string.")
130 private static Function lower = new MixedModeFunction("lower",
131 ImmutableList.of("this"), 1, false) {
132 @Override
133 public Object call(Object[] args, FuncallExpression ast) throws ConversionException {
134 String thiz = Type.STRING.convert(args[0], "'lower' operand");
135 return thiz.toLowerCase();
136 }
137 };
138
139 @SkylarkBuiltin(name = "upper", objectType = StringModule.class, returnType = String.class,
140 doc = "Returns the upper case version of this string.")
141 private static Function upper = new MixedModeFunction("upper",
142 ImmutableList.of("this"), 1, false) {
143 @Override
144 public Object call(Object[] args, FuncallExpression ast) throws ConversionException {
145 String thiz = Type.STRING.convert(args[0], "'upper' operand");
146 return thiz.toUpperCase();
147 }
148 };
149
150 @SkylarkBuiltin(name = "replace", objectType = StringModule.class, returnType = String.class,
151 doc = "Returns a copy of the string in which the occurrences "
152 + "of <code>old</code> have been replaced with <code>new</code>, optionally restricting "
153 + "the number of replacements to <code>maxsplit</code>.",
154 mandatoryParams = {
155 @Param(name = "old", type = String.class, doc = "The string to be replaced."),
156 @Param(name = "new", type = String.class, doc = "The string to replace with.")},
157 optionalParams = {
158 @Param(name = "maxsplit", type = Integer.class, doc = "The maximum number of replacements.")})
159 private static Function replace =
160 new MixedModeFunction("replace", ImmutableList.of("this", "old", "new", "maxsplit"), 3, false) {
161 @Override
162 public Object call(Object[] args, FuncallExpression ast) throws EvalException,
163 ConversionException {
164 String thiz = Type.STRING.convert(args[0], "'replace' operand");
165 String old = Type.STRING.convert(args[1], "'replace' argument");
166 String neww = Type.STRING.convert(args[2], "'replace' argument");
167 int maxsplit =
168 args[3] != null ? Type.INTEGER.convert(args[3], "'replace' argument")
169 : Integer.MAX_VALUE;
170 StringBuffer sb = new StringBuffer();
171 try {
172 Matcher m = Pattern.compile(old, Pattern.LITERAL).matcher(thiz);
173 for (int i = 0; i < maxsplit && m.find(); i++) {
174 m.appendReplacement(sb, Matcher.quoteReplacement(neww));
175 }
176 m.appendTail(sb);
177 } catch (IllegalStateException e) {
178 throw new EvalException(ast.getLocation(), e.getMessage() + " in call to replace");
179 }
180 return sb.toString();
181 }
182 };
183
184 @SkylarkBuiltin(name = "split", objectType = StringModule.class, returnType = SkylarkList.class,
185 doc = "Returns a list of all the words in the string, using <code>sep</code> "
186 + "as the separator, optionally limiting the number of splits to <code>maxsplit</code>.",
187 optionalParams = {
188 @Param(name = "sep", type = String.class,
189 doc = "The string to split on, default is space (\" \")."),
190 @Param(name = "maxsplit", type = Integer.class, doc = "The maximum number of splits.")})
191 private static Function split = new MixedModeFunction("split",
192 ImmutableList.of("this", "sep", "maxsplit"), 1, false) {
193 @Override
194 public Object call(Object[] args, FuncallExpression ast, Environment env)
195 throws ConversionException {
196 String thiz = Type.STRING.convert(args[0], "'split' operand");
197 String sep = args[1] != null
198 ? Type.STRING.convert(args[1], "'split' argument")
199 : " ";
200 int maxsplit = args[2] != null
201 ? Type.INTEGER.convert(args[2], "'split' argument") + 1 // last is remainder
202 : -1;
203 String[] ss = Pattern.compile(sep, Pattern.LITERAL).split(thiz,
204 maxsplit);
205 List<String> result = java.util.Arrays.asList(ss);
206 return env.isSkylarkEnabled() ? SkylarkList.list(result, String.class) : result;
207 }
208 };
209
210 @SkylarkBuiltin(name = "rfind", objectType = StringModule.class, returnType = Integer.class,
211 doc = "Returns the last index where <code>sub</code> is found, "
212 + "or -1 if no such index exists, optionally restricting to "
213 + "[<code>start</code>:<code>end</code>], "
214 + "<code>start</code> being inclusive and <code>end</code> being exclusive.",
215 mandatoryParams = {
216 @Param(name = "sub", type = String.class, doc = "The substring to find.")},
217 optionalParams = {
218 @Param(name = "start", type = Integer.class, doc = "Restrict to search from this position."),
219 @Param(name = "end", type = Integer.class, doc = "Restrict to search before this position.")})
220 private static Function rfind =
221 new MixedModeFunction("rfind", ImmutableList.of("this", "sub", "start", "end"), 2, false) {
222 @Override
223 public Object call(Object[] args, FuncallExpression ast)
224 throws ConversionException {
225 String thiz = Type.STRING.convert(args[0], "'rfind' operand");
226 String sub = Type.STRING.convert(args[1], "'rfind' argument");
227 int start = 0;
228 if (args[2] != null) {
229 start = Type.INTEGER.convert(args[2], "'rfind' argument");
230 }
231 int end = thiz.length();
232 if (args[3] != null) {
233 end = Type.INTEGER.convert(args[3], "'rfind' argument");
234 }
235 int subpos = getPythonSubstring(thiz, start, end).lastIndexOf(sub);
236 start = getPythonStringIndex(start, thiz.length());
237 return subpos < 0 ? subpos : subpos + start;
238 }
239 };
240
241 @SkylarkBuiltin(name = "find", objectType = StringModule.class, returnType = Integer.class,
242 doc = "Returns the first index where <code>sub</code> is found, "
243 + "or -1 if no such index exists, optionally restricting to "
244 + "[<code>start</code>:<code>end]</code>, "
245 + "<code>start</code> being inclusive and <code>end</code> being exclusive.",
246 mandatoryParams = {
247 @Param(name = "sub", type = String.class, doc = "The substring to find.")},
248 optionalParams = {
249 @Param(name = "start", type = Integer.class, doc = "Restrict to search from this position."),
250 @Param(name = "end", type = Integer.class, doc = "Restrict to search before this position.")})
251 private static Function find =
252 new MixedModeFunction("find", ImmutableList.of("this", "sub", "start", "end"), 2, false) {
253 @Override
254 public Object call(Object[] args, FuncallExpression ast)
255 throws ConversionException {
256 String thiz = Type.STRING.convert(args[0], "'find' operand");
257 String sub = Type.STRING.convert(args[1], "'find' argument");
258 int start = 0;
259 if (args[2] != null) {
260 start = Type.INTEGER.convert(args[2], "'find' argument");
261 }
262 int end = thiz.length();
263 if (args[3] != null) {
264 end = Type.INTEGER.convert(args[3], "'find' argument");
265 }
266 int subpos = getPythonSubstring(thiz, start, end).indexOf(sub);
267 start = getPythonStringIndex(start, thiz.length());
268 return subpos < 0 ? subpos : subpos + start;
269 }
270 };
271
272 @SkylarkBuiltin(name = "count", objectType = StringModule.class, returnType = Integer.class,
273 doc = "Returns the number of (non-overlapping) occurrences of substring <code>sub</code> in "
274 + "string, optionally restricting to [<code>start</code>:<code>end</code>], "
275 + "<code>start</code> being inclusive and <code>end</code> being exclusive.",
276 mandatoryParams = {
277 @Param(name = "sub", type = String.class, doc = "The substring to count.")},
278 optionalParams = {
279 @Param(name = "start", type = Integer.class, doc = "Restrict to search from this position."),
280 @Param(name = "end", type = Integer.class, doc = "Restrict to search before this position.")})
281 private static Function count =
282 new MixedModeFunction("count", ImmutableList.of("this", "sub", "start", "end"), 2, false) {
283 @Override
284 public Object call(Object[] args, FuncallExpression ast)
285 throws ConversionException {
286 String thiz = Type.STRING.convert(args[0], "'count' operand");
287 String sub = Type.STRING.convert(args[1], "'count' argument");
288 int start = 0;
289 if (args[2] != null) {
290 start = Type.INTEGER.convert(args[2], "'count' argument");
291 }
292 int end = thiz.length();
293 if (args[3] != null) {
294 end = Type.INTEGER.convert(args[3], "'count' argument");
295 }
296 String str = getPythonSubstring(thiz, start, end);
297 if (sub.equals("")) {
298 return str.length() + 1;
299 }
300 int count = 0;
301 int index = -1;
302 while ((index = str.indexOf(sub)) >= 0) {
303 count++;
304 str = str.substring(index + sub.length());
305 }
306 return count;
307 }
308 };
309
310 @SkylarkBuiltin(name = "endswith", objectType = StringModule.class, returnType = Boolean.class,
311 doc = "Returns True if the string ends with <code>sub</code>, "
312 + "otherwise False, optionally restricting to [<code>start</code>:<code>end</code>], "
313 + "<code>start</code> being inclusive and <code>end</code> being exclusive.",
314 mandatoryParams = {
315 @Param(name = "sub", type = String.class, doc = "The substring to check.")},
316 optionalParams = {
317 @Param(name = "start", type = Integer.class, doc = "Test beginning at this position."),
318 @Param(name = "end", type = Integer.class, doc = "Stop comparing at this position.")})
319 private static Function endswith =
320 new MixedModeFunction("endswith", ImmutableList.of("this", "sub", "start", "end"), 2, false) {
321 @Override
322 public Object call(Object[] args, FuncallExpression ast)
323 throws ConversionException {
324 String thiz = Type.STRING.convert(args[0], "'endswith' operand");
325 String sub = Type.STRING.convert(args[1], "'endswith' argument");
326 int start = 0;
327 if (args[2] != null) {
328 start = Type.INTEGER.convert(args[2], "'endswith' argument");
329 }
330 int end = thiz.length();
331 if (args[3] != null) {
332 end = Type.INTEGER.convert(args[3], "");
333 }
334
335 return getPythonSubstring(thiz, start, end).endsWith(sub);
336 }
337 };
338
339 @SkylarkBuiltin(name = "startswith", objectType = StringModule.class, returnType = Boolean.class,
340 doc = "Returns True if the string starts with <code>sub</code>, "
341 + "otherwise False, optionally restricting to [<code>start</code>:<code>end</code>], "
342 + "<code>start</code> being inclusive and <code>end</code> being exclusive.",
343 mandatoryParams = {
344 @Param(name = "sub", type = String.class, doc = "The substring to check.")},
345 optionalParams = {
346 @Param(name = "start", type = Integer.class, doc = "Test beginning at this position."),
347 @Param(name = "end", type = Integer.class, doc = "Stop comparing at this position.")})
348 private static Function startswith =
349 new MixedModeFunction("startswith", ImmutableList.of("this", "sub", "start", "end"), 2, false) {
350 @Override
351 public Object call(Object[] args, FuncallExpression ast) throws ConversionException {
352 String thiz = Type.STRING.convert(args[0], "'startswith' operand");
353 String sub = Type.STRING.convert(args[1], "'startswith' argument");
354 int start = 0;
355 if (args[2] != null) {
356 start = Type.INTEGER.convert(args[2], "'startswith' argument");
357 }
358 int end = thiz.length();
359 if (args[3] != null) {
360 end = Type.INTEGER.convert(args[3], "'startswith' argument");
361 }
362 return getPythonSubstring(thiz, start, end).startsWith(sub);
363 }
364 };
365
366 // TODO(bazel-team): Maybe support an argument to tell the type of the whitespace.
367 @SkylarkBuiltin(name = "strip", objectType = StringModule.class, returnType = String.class,
368 doc = "Returns a copy of the string in which all whitespace characters "
369 + "have been stripped from the beginning and the end of the string.")
370 private static Function strip =
371 new MixedModeFunction("strip", ImmutableList.of("this"), 1, false) {
372 @Override
373 public Object call(Object[] args, FuncallExpression ast)
374 throws ConversionException {
375 String operand = Type.STRING.convert(args[0], "'strip' operand");
376 return operand.trim();
377 }
378 };
379
380 // substring operator
381 @SkylarkBuiltin(name = "$substring", hidden = true,
382 doc = "String[<code>start</code>:<code>end</code>] returns a substring.")
383 private static Function substring = new MixedModeFunction("$substring",
384 ImmutableList.of("this", "start", "end"), 3, false) {
385 @Override
386 public Object call(Object[] args, FuncallExpression ast) throws ConversionException {
387 String thiz = Type.STRING.convert(args[0], "substring operand");
388 int left = Type.INTEGER.convert(args[1], "substring operand");
389 int right = Type.INTEGER.convert(args[2], "substring operand");
390 return getPythonSubstring(thiz, left, right);
391 }
392 };
393
394 // supported list methods
395 @SkylarkBuiltin(name = "append", hidden = true,
396 doc = "Adds an item to the end of the list.")
397 private static Function append = new MixedModeFunction("append",
398 ImmutableList.of("this", "x"), 2, false) {
399 @Override
400 public Object call(Object[] args, FuncallExpression ast) throws EvalException,
401 ConversionException {
402 List<Object> thiz = Type.OBJECT_LIST.convert(args[0], "'append' operand");
403 thiz.add(args[1]);
404 return Environment.NONE;
405 }
406 };
407
408 @SkylarkBuiltin(name = "extend", hidden = true,
409 doc = "Adds all items to the end of the list.")
410 private static Function extend = new MixedModeFunction("extend",
411 ImmutableList.of("this", "x"), 2, false) {
412 @Override
413 public Object call(Object[] args, FuncallExpression ast) throws EvalException,
414 ConversionException {
415 List<Object> thiz = Type.OBJECT_LIST.convert(args[0], "'extend' operand");
416 List<Object> l = Type.OBJECT_LIST.convert(args[1], "'extend' argument");
417 thiz.addAll(l);
418 return Environment.NONE;
419 }
420 };
421
422 // dictionary access operator
423 @SkylarkBuiltin(name = "$index", hidden = true,
424 doc = "Returns the nth element of a list or string, "
425 + "or looks up a value in a dictionary.")
426 private static Function index = new MixedModeFunction("$index",
427 ImmutableList.of("this", "index"), 2, false) {
428 @Override
429 public Object call(Object[] args, FuncallExpression ast) throws EvalException,
430 ConversionException {
431 Object collectionCandidate = args[0];
432 Object key = args[1];
433
434 if (collectionCandidate instanceof Map<?, ?>) {
435 Map<?, ?> dictionary = (Map<?, ?>) collectionCandidate;
436 if (!dictionary.containsKey(key)) {
437 throw new EvalException(ast.getLocation(), "Key '" + key + "' not found in dictionary");
438 }
439 return dictionary.get(key);
440 } else if (collectionCandidate instanceof List<?>) {
441
442 List<Object> list = Type.OBJECT_LIST.convert(collectionCandidate, "index operand");
443
444 if (!list.isEmpty()) {
445 int index = getListIndex(key, list.size(), ast);
446 return list.get(index);
447 }
448
449 throw new EvalException(ast.getLocation(), "List is empty");
450 } else if (collectionCandidate instanceof SkylarkList) {
451 SkylarkList list = (SkylarkList) collectionCandidate;
452
453 if (!list.isEmpty()) {
454 int index = getListIndex(key, list.size(), ast);
455 return list.get(index);
456 }
457
458 throw new EvalException(ast.getLocation(), "List is empty");
459 } else if (collectionCandidate instanceof String) {
460 String str = (String) collectionCandidate;
461 int index = getListIndex(key, str.length(), ast);
462 return str.substring(index, index + 1);
463
464 } else {
465 // TODO(bazel-team): This is dead code, get rid of it.
466 throw new EvalException(ast.getLocation(), String.format(
467 "Unsupported datatype (%s) for indexing, only works for dict and list",
Francois-Rene Rideaucbebd632015-02-11 16:56:37 +0000468 EvalUtils.getDataTypeName(collectionCandidate)));
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100469 }
470 }
471 };
472
473 @SkylarkBuiltin(name = "values", objectType = DictModule.class, returnType = SkylarkList.class,
474 doc = "Return the list of values.")
475 private static Function values = new NoArgFunction("values") {
476 @Override
477 public Object call(Object self, FuncallExpression ast, Environment env)
478 throws EvalException, InterruptedException {
479 Map<?, ?> dict = (Map<?, ?>) self;
480 return convert(dict.values(), env, ast.getLocation());
481 }
482 };
483
484 @SkylarkBuiltin(name = "items", objectType = DictModule.class, returnType = SkylarkList.class,
485 doc = "Return the list of key-value tuples.")
486 private static Function items = new NoArgFunction("items") {
487 @Override
488 public Object call(Object self, FuncallExpression ast, Environment env)
489 throws EvalException, InterruptedException {
490 Map<?, ?> dict = (Map<?, ?>) self;
491 List<Object> list = Lists.newArrayListWithCapacity(dict.size());
492 for (Map.Entry<?, ?> entries : dict.entrySet()) {
493 List<?> item = ImmutableList.of(entries.getKey(), entries.getValue());
494 list.add(env.isSkylarkEnabled() ? SkylarkList.tuple(item) : item);
495 }
496 return convert(list, env, ast.getLocation());
497 }
498 };
499
500 @SkylarkBuiltin(name = "keys", objectType = DictModule.class, returnType = SkylarkList.class,
501 doc = "Return the list of keys.")
502 private static Function keys = new NoArgFunction("keys") {
503 @Override
504 public Object call(Object self, FuncallExpression ast, Environment env)
505 throws EvalException, InterruptedException {
506 Map<?, ?> dict = (Map<?, ?>) self;
507 return convert(dict.keySet(), env, ast.getLocation());
508 }
509 };
510
511 @SuppressWarnings("unchecked")
512 private static Iterable<Object> convert(Collection<?> list, Environment env, Location loc)
513 throws EvalException {
514 if (env.isSkylarkEnabled()) {
515 return SkylarkList.list(list, loc);
516 } else {
517 return Lists.newArrayList(list);
518 }
519 }
520
521 // unary minus
522 @SkylarkBuiltin(name = "-", hidden = true, doc = "Unary minus operator.")
523 private static Function minus = new MixedModeFunction("-", ImmutableList.of("this"), 1, false) {
524 @Override
525 public Object call(Object[] args, FuncallExpression ast) throws ConversionException {
526 int num = Type.INTEGER.convert(args[0], "'unary minus' argument");
527 return -num;
528 }
529 };
530
531 @SkylarkBuiltin(name = "list", returnType = SkylarkList.class,
532 doc = "Converts a collection (e.g. set or dictionary) to a list.",
533 mandatoryParams = {@Param(name = "x", doc = "The object to convert.")})
534 private static Function list = new MixedModeFunction("list",
535 ImmutableList.of("list"), 1, false) {
536 @Override
537 public Object call(Object[] args, FuncallExpression ast) throws EvalException {
538 Location loc = ast.getLocation();
539 return SkylarkList.list(EvalUtils.toCollection(args[0], loc), loc);
540 }
541 };
542
543 @SkylarkBuiltin(name = "len", returnType = Integer.class, doc =
544 "Returns the length of a string, list, tuple, set, or dictionary.",
545 mandatoryParams = {@Param(name = "x", doc = "The object to check length of.")})
546 private static Function len = new MixedModeFunction("len",
547 ImmutableList.of("list"), 1, false) {
548
549 @Override
550 public Object call(Object[] args, FuncallExpression ast) throws EvalException {
551 Object arg = args[0];
552 int l = EvalUtils.size(arg);
553 if (l == -1) {
554 throw new EvalException(ast.getLocation(),
Francois-Rene Rideaucbebd632015-02-11 16:56:37 +0000555 EvalUtils.getDataTypeName(arg) + " is not iterable");
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100556 }
557 return l;
558 }
559 };
560
561 @SkylarkBuiltin(name = "str", returnType = String.class, doc =
562 "Converts any object to string. This is useful for debugging.",
563 mandatoryParams = {@Param(name = "x", doc = "The object to convert.")})
564 private static Function str = new MixedModeFunction("str", ImmutableList.of("this"), 1, false) {
565 @Override
566 public Object call(Object[] args, FuncallExpression ast) throws EvalException {
567 return EvalUtils.printValue(args[0]);
568 }
569 };
570
571 @SkylarkBuiltin(name = "bool", returnType = Boolean.class, doc = "Converts an object to boolean. "
572 + "It returns False if the object is None, False, an empty string, the number 0, or an "
573 + "empty collection. Otherwise, it returns True.",
574 mandatoryParams = {@Param(name = "x", doc = "The variable to convert.")})
575 private static Function bool = new MixedModeFunction("bool",
576 ImmutableList.of("this"), 1, false) {
577 @Override
578 public Object call(Object[] args, FuncallExpression ast) throws EvalException {
579 return EvalUtils.toBoolean(args[0]);
580 }
581 };
582
583 @SkylarkBuiltin(name = "struct", returnType = SkylarkClassObject.class, doc =
584 "Creates an immutable struct using the keyword arguments as fields. It is used to group "
585 + "multiple values together.Example:<br>"
586 + "<pre class=language-python>s = struct(x = 2, y = 3)\n"
587 + "return s.x + s.y # returns 5</pre>")
588 private static Function struct = new AbstractFunction("struct") {
589
590 @Override
591 public Object call(List<Object> args, Map<String, Object> kwargs, FuncallExpression ast,
592 Environment env) throws EvalException, InterruptedException {
593 if (args.size() > 0) {
594 throw new EvalException(ast.getLocation(), "struct only supports keyword arguments");
595 }
596 return new SkylarkClassObject(kwargs, ast.getLocation());
597 }
598 };
599
600 @SkylarkBuiltin(name = "set", returnType = SkylarkNestedSet.class,
Laszlo Csomoraded1c22015-02-16 16:57:12 +0000601 doc = "Creates a set from the <code>items</code>. The set supports nesting other sets of the"
602 + " same element type in it. For this reason sets are also referred to as <i>nested sets</i>"
603 + " (all Skylark sets are nested sets). A desired iteration order can also be specified.<br>"
604 + " Examples:<br><pre class=language-python>set([1, set([2, 3]), 2])\n"
605 + "set([1, 2, 3], order=\"compile\")</pre>",
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100606 optionalParams = {
607 @Param(name = "items", type = SkylarkList.class,
Laszlo Csomoraded1c22015-02-16 16:57:12 +0000608 doc = "The items to initialize the set with. May contain both standalone items and other"
609 + " sets."),
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100610 @Param(name = "order", type = String.class,
611 doc = "The ordering strategy for the set if it's nested, "
612 + "possible values are: <code>stable</code> (default), <code>compile</code>, "
613 + "<code>link</code> or <code>naive_link</code>.")})
614 private static final Function set =
615 new MixedModeFunction("set", ImmutableList.of("items", "order"), 0, false) {
616 @Override
617 public Object call(Object[] args, FuncallExpression ast) throws EvalException,
618 ConversionException {
619 Order order;
620 if (args[1] == null || args[1].equals("stable")) {
621 order = Order.STABLE_ORDER;
622 } else if (args[1].equals("compile")) {
623 order = Order.COMPILE_ORDER;
624 } else if (args[1].equals("link")) {
625 order = Order.LINK_ORDER;
626 } else if (args[1].equals("naive_link")) {
627 order = Order.NAIVE_LINK_ORDER;
628 } else {
629 throw new EvalException(ast.getLocation(), "Invalid order: " + args[1]);
630 }
631
632 if (args[0] == null) {
633 return new SkylarkNestedSet(order, SkylarkList.EMPTY_LIST, ast.getLocation());
634 }
635 return new SkylarkNestedSet(order, args[0], ast.getLocation());
636 }
637 };
638
639 @SkylarkBuiltin(name = "enumerate", returnType = SkylarkList.class,
Laszlo Csomoraded1c22015-02-16 16:57:12 +0000640 doc = "Return a list of pairs (two-element lists), with the index (int) and the item from"
641 + " the input list.\n<pre class=language-python>"
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100642 + "enumerate([24, 21, 84]) == [[0, 24], [1, 21], [2, 84]]</pre>\n",
643 mandatoryParams = {
644 @Param(name = "list", type = SkylarkList.class,
645 doc = "input list"),
646 })
647 private static Function enumerate = new MixedModeFunction("enumerate",
648 ImmutableList.of("list"), 1, false) {
649 @Override
650 public Object call(Object[] args, FuncallExpression ast) throws EvalException,
651 ConversionException {
652 List<Object> input = Type.OBJECT_LIST.convert(args[0], "'enumerate' operand");
653 List<List<Object>> result = Lists.newArrayList();
654 int count = 0;
655 for (Object obj : input) {
656 result.add(Lists.newArrayList(count, obj));
657 count++;
658 }
659 return result;
660 }
661 };
662
663 @SkylarkBuiltin(name = "range", returnType = SkylarkList.class,
Laszlo Csomoraded1c22015-02-16 16:57:12 +0000664 doc = "Creates a list where items go from <code>start</code> to <code>end</code>, using a "
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100665 + "<code>step</code> increment. If a single argument is provided, items will "
666 + "range from 0 to that element."
667 + "<pre class=language-python>range(4) == [0, 1, 2, 3]\n"
668 + "range(3, 9, 2) == [3, 5, 7]\n"
669 + "range(3, 0, -1) == [3, 2, 1]</pre>",
670 mandatoryParams = {
671 @Param(name = "start", type = Integer.class,
672 doc = "Value of the first element"),
673 },
674 optionalParams = {
Laszlo Csomoraded1c22015-02-16 16:57:12 +0000675 @Param(name = "end", type = Integer.class,
676 doc = "The first item <i>not</i> to be included in the resulting list; "
677 + "generation of the list stops before <code>end</code> is reached."),
678 @Param(name = "step", type = Integer.class,
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100679 doc = "The increment (default is 1). It may be negative.")})
680 private static final Function range =
681 new MixedModeFunction("range", ImmutableList.of("start", "stop", "step"), 1, false) {
682 @Override
683 public Object call(Object[] args, FuncallExpression ast) throws EvalException,
684 ConversionException {
685 int start;
686 int stop;
687 if (args[1] == null) {
688 start = 0;
Laszlo Csomoraded1c22015-02-16 16:57:12 +0000689 stop = Type.INTEGER.convert(args[0], "end");
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100690 } else {
691 start = Type.INTEGER.convert(args[0], "start");
Laszlo Csomoraded1c22015-02-16 16:57:12 +0000692 stop = Type.INTEGER.convert(args[1], "end");
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100693 }
694 int step = args[2] == null ? 1 : Type.INTEGER.convert(args[2], "step");
695 if (step == 0) {
696 throw new EvalException(ast.getLocation(), "step cannot be 0");
697 }
698 List<Integer> result = Lists.newArrayList();
699 if (step > 0) {
700 while (start < stop) {
701 result.add(start);
702 start += step;
703 }
704 } else {
705 while (start > stop) {
706 result.add(start);
707 start += step;
708 }
709 }
710 return SkylarkList.list(result, Integer.class);
711 }
712 };
713
714 /**
715 * Returns a function-value implementing "select" (i.e. configurable attributes)
716 * in the specified package context.
717 */
718 @SkylarkBuiltin(name = "select",
719 doc = "Creates a SelectorValue from the dict parameter.",
720 mandatoryParams = {@Param(name = "x", type = Map.class, doc = "The parameter to convert.")})
721 private static final Function select = new MixedModeFunction("select",
722 ImmutableList.of("x"), 1, false) {
723 @Override
724 public Object call(Object[] args, FuncallExpression ast)
725 throws EvalException, ConversionException {
726 Object dict = args[0];
727 if (!(dict instanceof Map<?, ?>)) {
728 throw new EvalException(ast.getLocation(),
729 "select({...}) argument isn't a dictionary");
730 }
731 return new SelectorValue((Map<?, ?>) dict);
732 }
733 };
734
735 /**
736 * Returns true if the object has a field of the given name, otherwise false.
737 */
738 @SkylarkBuiltin(name = "hasattr", returnType = Boolean.class,
739 doc = "Returns True if the object <code>x</code> has a field of the given <code>name</code>, "
740 + "otherwise False. Example:<br>"
741 + "<pre class=language-python>hasattr(ctx.attr, \"myattr\")</pre>",
742 mandatoryParams = {
743 @Param(name = "object", doc = "The object to check."),
744 @Param(name = "name", type = String.class, doc = "The name of the field.")})
745 private static final Function hasattr =
746 new MixedModeFunction("hasattr", ImmutableList.of("object", "name"), 2, false) {
747
748 @Override
749 public Object call(Object[] args, FuncallExpression ast, Environment env)
750 throws EvalException, ConversionException {
751 Object obj = args[0];
752 String name = cast(args[1], String.class, "name", ast.getLocation());
753
754 if (obj instanceof ClassObject && ((ClassObject) obj).getValue(name) != null) {
755 return true;
756 }
757
758 if (env.getFunctionNames(obj.getClass()).contains(name)) {
759 return true;
760 }
761
762 try {
763 return FuncallExpression.getMethodNames(obj.getClass()).contains(name);
764 } catch (ExecutionException e) {
765 // This shouldn't happen
766 throw new EvalException(ast.getLocation(), e.getMessage());
767 }
768 }
769 };
770
771 @SkylarkBuiltin(name = "getattr",
772 doc = "Returns the struct's field of the given name if exists, otherwise <code>default</code>"
773 + " if specified, otherwise rasies an error. For example, <code>getattr(x, \"foobar\")"
774 + "</code> is equivalent to <code>x.foobar</code>."
775 + "Example:<br>"
776 + "<pre class=language-python>getattr(ctx.attr, \"myattr\")\n"
777 + "getattr(ctx.attr, \"myattr\", \"mydefault\")</pre>",
778 mandatoryParams = {
779 @Param(name = "object", doc = "The struct which's field is accessed."),
780 @Param(name = "name", doc = "The name of the struct field.")},
781 optionalParams = {
782 @Param(name = "default", doc = "The default value to return in case the struct "
783 + "doesn't have a field of the given name.")})
784 private static final Function getattr = new MixedModeFunction(
785 "getattr", ImmutableList.of("object", "name", "default"), 2, false) {
786 @Override
787 public Object call(Object[] args, FuncallExpression ast, Environment env)
788 throws EvalException {
789 Object obj = args[0];
790 String name = cast(args[1], String.class, "name", ast.getLocation());
791 Object result = DotExpression.eval(obj, name, ast.getLocation());
792 if (result == null) {
793 if (args[2] != null) {
794 return args[2];
795 } else {
796 throw new EvalException(ast.getLocation(), "Object of type '"
Francois-Rene Rideaucbebd632015-02-11 16:56:37 +0000797 + EvalUtils.getDataTypeName(obj) + "' has no field '" + name + "'");
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100798 }
799 }
800 return result;
801 }
802 };
803
804 @SkylarkBuiltin(name = "dir", returnType = SkylarkList.class,
Laszlo Csomoraded1c22015-02-16 16:57:12 +0000805 doc = "Returns a list strings: the names of the fields and "
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100806 + "methods of the parameter object.",
807 mandatoryParams = {@Param(name = "object", doc = "The object to check.")})
808 private static final Function dir = new MixedModeFunction(
809 "dir", ImmutableList.of("object"), 1, false) {
810 @Override
811 public Object call(Object[] args, FuncallExpression ast, Environment env)
812 throws EvalException {
813 Object obj = args[0];
814 // Order the fields alphabetically.
815 Set<String> fields = new TreeSet<>();
816 if (obj instanceof ClassObject) {
817 fields.addAll(((ClassObject) obj).getKeys());
818 }
819 fields.addAll(env.getFunctionNames(obj.getClass()));
820 try {
821 fields.addAll(FuncallExpression.getMethodNames(obj.getClass()));
822 } catch (ExecutionException e) {
823 // This shouldn't happen
824 throw new EvalException(ast.getLocation(), e.getMessage());
825 }
826 return SkylarkList.list(fields, String.class);
827 }
828 };
829
830 @SkylarkBuiltin(name = "type", returnType = String.class,
831 doc = "Returns the type name of its argument.",
832 mandatoryParams = {@Param(name = "object", doc = "The object to check type of.")})
833 private static final Function type = new MixedModeFunction("type",
834 ImmutableList.of("object"), 1, false) {
835 @Override
836 public Object call(Object[] args, FuncallExpression ast) throws EvalException {
837 // There is no 'type' type in Skylark, so we return a string with the type name.
Francois-Rene Rideaucbebd632015-02-11 16:56:37 +0000838 return EvalUtils.getDataTypeName(args[0]);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100839 }
840 };
841
842 @SkylarkBuiltin(name = "fail",
843 doc = "Raises an error (the execution stops), except if the <code>when</code> condition "
844 + "is False.",
845 returnType = Environment.NoneType.class,
846 mandatoryParams = {
847 @Param(name = "msg", type = String.class, doc = "Error message to display for the user")},
848 optionalParams = {
849 @Param(name = "attr", type = String.class,
850 doc = "The name of the attribute that caused the error"),
851 @Param(name = "when", type = Boolean.class,
852 doc = "When False, the function does nothing. Default is True.")})
853 private static final Function fail = new MixedModeFunction(
854 "fail", ImmutableList.of("msg", "attr", "when"), 1, false) {
855 @Override
856 public Object call(Object[] args, FuncallExpression ast, Environment env)
857 throws EvalException {
858 if (args[2] != null) {
859 if (!EvalUtils.toBoolean(args[2])) {
860 return Environment.NONE;
861 }
862 }
863 String msg = cast(args[0], String.class, "msg", ast.getLocation());
864 if (args[1] != null) {
865 msg = "attribute " + cast(args[1], String.class, "attr", ast.getLocation())
866 + ": " + msg;
867 }
868 throw new EvalException(ast.getLocation(), msg);
869 }
870 };
871
872 @SkylarkBuiltin(name = "print", returnType = Environment.NoneType.class,
873 doc = "Prints <code>msg</code> to the console.",
874 mandatoryParams = {
875 @Param(name = "*args", doc = "The objects to print.")},
876 optionalParams = {
877 @Param(name = "sep", type = String.class,
878 doc = "The separator string between the objects, default is space (\" \").")})
879 private static final Function print = new AbstractFunction("print") {
880 @Override
881 public Object call(List<Object> args, Map<String, Object> kwargs, FuncallExpression ast,
882 Environment env) throws EvalException, InterruptedException {
883 String sep = " ";
884 if (kwargs.containsKey("sep")) {
885 sep = cast(kwargs.remove("sep"), String.class, "sep", ast.getLocation());
886 }
887 if (kwargs.size() > 0) {
888 throw new EvalException(ast.getLocation(),
889 "unexpected keywords: '" + kwargs.keySet() + "'");
890 }
891 String msg = Joiner.on(sep).join(Iterables.transform(args,
892 new com.google.common.base.Function<Object, String>() {
893 @Override
894 public String apply(Object input) {
895 return EvalUtils.printValue(input);
896 }
897 }));
898 ((SkylarkEnvironment) env).handleEvent(Event.warn(ast.getLocation(), msg));
899 return Environment.NONE;
900 }
901 };
902
903 /**
904 * Skylark String module.
905 */
906 @SkylarkModule(name = "string", doc =
907 "A language built-in type to support strings. "
908 + "Example of string literals:<br>"
909 + "<pre class=language-python>a = 'abc\\ndef'\n"
910 + "b = \"ab'cd\"\n"
911 + "c = \"\"\"multiline string\"\"\"</pre>"
912 + "Strings are iterable and support the <code>in</code> operator. Examples:<br>"
913 + "<pre class=language-python>\"a\" in \"abc\" # evaluates as True\n"
Laszlo Csomoraded1c22015-02-16 16:57:12 +0000914 + "x = []\n"
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100915 + "for s in \"abc\":\n"
Laszlo Csomoraded1c22015-02-16 16:57:12 +0000916 + " x += [s] # x == [\"a\", \"b\", \"c\"]</pre>")
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100917 public static final class StringModule {}
918
919 /**
920 * Skylark Dict module.
921 */
922 @SkylarkModule(name = "dict", doc =
923 "A language built-in type to support dicts. "
924 + "Example of dict literal:<br>"
925 + "<pre class=language-python>d = {\"a\": 2, \"b\": 5}</pre>"
926 + "Accessing elements works just like in Python:<br>"
927 + "<pre class=language-python>e = d[\"a\"] # e == 2</pre>"
928 + "Dicts support the <code>+</code> operator to concatenate two dicts. In case of multiple "
929 + "keys the second one overrides the first one. Examples:<br>"
930 + "<pre class=language-python>"
931 + "d = {\"a\" : 1} + {\"b\" : 2} # d == {\"a\" : 1, \"b\" : 2}\n"
932 + "d += {\"c\" : 3} # d == {\"a\" : 1, \"b\" : 2, \"c\" : 3}\n"
933 + "d = d + {\"c\" : 5} # d == {\"a\" : 1, \"b\" : 2, \"c\" : 5}</pre>"
934 + "Since the language doesn't have mutable objects <code>d[\"a\"] = 5</code> automatically "
935 + "translates to <code>d = d + {\"a\" : 5}</code>.<br>"
936 + "Dicts are iterable, the iteration works on their keyset.<br>"
937 + "Dicts support the <code>in</code> operator, testing membership in the keyset of the dict. "
938 + "Example:<br>"
939 + "<pre class=language-python>\"a\" in {\"a\" : 2, \"b\" : 5} # evaluates as True</pre>")
940 public static final class DictModule {}
941
942 public static final Map<Function, SkylarkType> stringFunctions = ImmutableMap
943 .<Function, SkylarkType>builder()
944 .put(join, SkylarkType.STRING)
945 .put(lower, SkylarkType.STRING)
946 .put(upper, SkylarkType.STRING)
947 .put(replace, SkylarkType.STRING)
948 .put(split, SkylarkType.of(List.class, String.class))
949 .put(rfind, SkylarkType.INT)
950 .put(find, SkylarkType.INT)
951 .put(endswith, SkylarkType.BOOL)
952 .put(startswith, SkylarkType.BOOL)
953 .put(strip, SkylarkType.STRING)
954 .put(substring, SkylarkType.STRING)
955 .put(count, SkylarkType.INT)
956 .build();
957
958 public static final List<Function> listFunctions = ImmutableList
959 .<Function>builder()
960 .add(append)
961 .add(extend)
962 .build();
963
964 public static final Map<Function, SkylarkType> dictFunctions = ImmutableMap
965 .<Function, SkylarkType>builder()
966 .put(items, SkylarkType.of(List.class))
967 .put(keys, SkylarkType.of(Set.class))
968 .put(values, SkylarkType.of(List.class))
969 .build();
970
971 private static final Map<Function, SkylarkType> pureGlobalFunctions = ImmutableMap
972 .<Function, SkylarkType>builder()
973 // TODO(bazel-team): String methods are added two times, because there are
974 // a lot of cases when they are used as global functions in the depot. Those
975 // should be cleaned up first.
976 .put(minus, SkylarkType.INT)
977 .put(select, SkylarkType.of(SelectorValue.class))
978 .put(len, SkylarkType.INT)
979 .put(str, SkylarkType.STRING)
980 .put(bool, SkylarkType.BOOL)
981 .build();
982
983 private static final Map<Function, SkylarkType> skylarkGlobalFunctions = ImmutableMap
984 .<Function, SkylarkType>builder()
985 .putAll(pureGlobalFunctions)
986 .put(list, SkylarkType.of(SkylarkList.class))
987 .put(struct, SkylarkType.of(ClassObject.class))
988 .put(hasattr, SkylarkType.BOOL)
989 .put(getattr, SkylarkType.UNKNOWN)
990 .put(set, SkylarkType.of(SkylarkNestedSet.class))
991 .put(dir, SkylarkType.of(SkylarkList.class, String.class))
992 .put(enumerate, SkylarkType.of(SkylarkList.class))
993 .put(range, SkylarkType.of(SkylarkList.class, Integer.class))
994 .put(type, SkylarkType.of(String.class))
995 .put(fail, SkylarkType.NONE)
996 .put(print, SkylarkType.NONE)
997 .build();
998
999 /**
1000 * Set up a given environment for supported class methods.
1001 */
1002 public static void setupMethodEnvironment(Environment env) {
1003 env.registerFunction(Map.class, index.getName(), index);
1004 setupMethodEnvironment(env, Map.class, dictFunctions.keySet());
1005 env.registerFunction(String.class, index.getName(), index);
1006 setupMethodEnvironment(env, String.class, stringFunctions.keySet());
1007 if (env.isSkylarkEnabled()) {
1008 env.registerFunction(SkylarkList.class, index.getName(), index);
1009 setupMethodEnvironment(env, skylarkGlobalFunctions.keySet());
1010 } else {
1011 env.registerFunction(List.class, index.getName(), index);
1012 env.registerFunction(ImmutableList.class, index.getName(), index);
1013 // TODO(bazel-team): listFunctions are not allowed in Skylark extensions (use += instead).
1014 // It is allowed in BUILD files only for backward-compatibility.
1015 setupMethodEnvironment(env, List.class, listFunctions);
1016 setupMethodEnvironment(env, stringFunctions.keySet());
1017 setupMethodEnvironment(env, pureGlobalFunctions.keySet());
1018 }
1019 }
1020
1021 private static void setupMethodEnvironment(
1022 Environment env, Class<?> nameSpace, Iterable<Function> functions) {
1023 for (Function function : functions) {
1024 env.registerFunction(nameSpace, function.getName(), function);
1025 }
1026 }
1027
1028 private static void setupMethodEnvironment(Environment env, Iterable<Function> functions) {
1029 for (Function function : functions) {
1030 env.update(function.getName(), function);
1031 }
1032 }
1033
1034 private static void setupValidationEnvironment(
1035 Map<Function, SkylarkType> functions, Map<String, SkylarkType> result) {
1036 for (Map.Entry<Function, SkylarkType> function : functions.entrySet()) {
1037 String name = function.getKey().getName();
1038 result.put(name, SkylarkFunctionType.of(name, function.getValue()));
1039 }
1040 }
1041
1042 public static void setupValidationEnvironment(
1043 Map<SkylarkType, Map<String, SkylarkType>> builtIn) {
1044 Map<String, SkylarkType> global = builtIn.get(SkylarkType.GLOBAL);
1045 setupValidationEnvironment(skylarkGlobalFunctions, global);
1046
1047 Map<String, SkylarkType> dict = new HashMap<>();
1048 setupValidationEnvironment(dictFunctions, dict);
1049 builtIn.put(SkylarkType.of(Map.class), dict);
1050
1051 Map<String, SkylarkType> string = new HashMap<>();
1052 setupValidationEnvironment(stringFunctions, string);
1053 builtIn.put(SkylarkType.STRING, string);
1054 }
1055}