Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 1 | // 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 | |
| 15 | package com.google.devtools.build.lib.packages; |
| 16 | |
| 17 | import static com.google.devtools.build.lib.syntax.SkylarkFunction.cast; |
| 18 | |
| 19 | import com.google.common.base.Joiner; |
| 20 | import com.google.common.collect.ImmutableList; |
| 21 | import com.google.common.collect.ImmutableMap; |
| 22 | import com.google.common.collect.Iterables; |
| 23 | import com.google.common.collect.Lists; |
| 24 | import com.google.devtools.build.lib.collect.nestedset.Order; |
| 25 | import com.google.devtools.build.lib.events.Event; |
| 26 | import com.google.devtools.build.lib.events.Location; |
| 27 | import com.google.devtools.build.lib.packages.Type.ConversionException; |
| 28 | import com.google.devtools.build.lib.syntax.AbstractFunction; |
| 29 | import com.google.devtools.build.lib.syntax.AbstractFunction.NoArgFunction; |
| 30 | import com.google.devtools.build.lib.syntax.ClassObject; |
| 31 | import com.google.devtools.build.lib.syntax.ClassObject.SkylarkClassObject; |
| 32 | import com.google.devtools.build.lib.syntax.DotExpression; |
| 33 | import com.google.devtools.build.lib.syntax.Environment; |
| 34 | import com.google.devtools.build.lib.syntax.EvalException; |
| 35 | import com.google.devtools.build.lib.syntax.EvalUtils; |
| 36 | import com.google.devtools.build.lib.syntax.FuncallExpression; |
| 37 | import com.google.devtools.build.lib.syntax.Function; |
| 38 | import com.google.devtools.build.lib.syntax.MixedModeFunction; |
| 39 | import com.google.devtools.build.lib.syntax.SelectorValue; |
| 40 | import com.google.devtools.build.lib.syntax.SkylarkBuiltin; |
| 41 | import com.google.devtools.build.lib.syntax.SkylarkBuiltin.Param; |
| 42 | import com.google.devtools.build.lib.syntax.SkylarkEnvironment; |
| 43 | import com.google.devtools.build.lib.syntax.SkylarkList; |
| 44 | import com.google.devtools.build.lib.syntax.SkylarkModule; |
| 45 | import com.google.devtools.build.lib.syntax.SkylarkNestedSet; |
| 46 | import com.google.devtools.build.lib.syntax.SkylarkType; |
| 47 | import com.google.devtools.build.lib.syntax.SkylarkType.SkylarkFunctionType; |
| 48 | |
| 49 | import java.util.Collection; |
| 50 | import java.util.HashMap; |
| 51 | import java.util.Iterator; |
| 52 | import java.util.List; |
| 53 | import java.util.Map; |
| 54 | import java.util.Set; |
| 55 | import java.util.TreeSet; |
| 56 | import java.util.concurrent.ExecutionException; |
| 57 | import java.util.regex.Matcher; |
| 58 | import java.util.regex.Pattern; |
| 59 | |
| 60 | /** |
| 61 | * A helper class containing built in functions for the Build and the Build Extension Language. |
| 62 | */ |
| 63 | public 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 Rideau | cbebd63 | 2015-02-11 16:56:37 +0000 | [diff] [blame] | 468 | EvalUtils.getDataTypeName(collectionCandidate))); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 469 | } |
| 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 Rideau | cbebd63 | 2015-02-11 16:56:37 +0000 | [diff] [blame] | 555 | EvalUtils.getDataTypeName(arg) + " is not iterable"); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 556 | } |
| 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 Csomor | aded1c2 | 2015-02-16 16:57:12 +0000 | [diff] [blame^] | 601 | 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 Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 606 | optionalParams = { |
| 607 | @Param(name = "items", type = SkylarkList.class, |
Laszlo Csomor | aded1c2 | 2015-02-16 16:57:12 +0000 | [diff] [blame^] | 608 | doc = "The items to initialize the set with. May contain both standalone items and other" |
| 609 | + " sets."), |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 610 | @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 Csomor | aded1c2 | 2015-02-16 16:57:12 +0000 | [diff] [blame^] | 640 | 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 Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 642 | + "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 Csomor | aded1c2 | 2015-02-16 16:57:12 +0000 | [diff] [blame^] | 664 | doc = "Creates a list where items go from <code>start</code> to <code>end</code>, using a " |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 665 | + "<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 Csomor | aded1c2 | 2015-02-16 16:57:12 +0000 | [diff] [blame^] | 675 | @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 Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 679 | 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 Csomor | aded1c2 | 2015-02-16 16:57:12 +0000 | [diff] [blame^] | 689 | stop = Type.INTEGER.convert(args[0], "end"); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 690 | } else { |
| 691 | start = Type.INTEGER.convert(args[0], "start"); |
Laszlo Csomor | aded1c2 | 2015-02-16 16:57:12 +0000 | [diff] [blame^] | 692 | stop = Type.INTEGER.convert(args[1], "end"); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 693 | } |
| 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 Rideau | cbebd63 | 2015-02-11 16:56:37 +0000 | [diff] [blame] | 797 | + EvalUtils.getDataTypeName(obj) + "' has no field '" + name + "'"); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 798 | } |
| 799 | } |
| 800 | return result; |
| 801 | } |
| 802 | }; |
| 803 | |
| 804 | @SkylarkBuiltin(name = "dir", returnType = SkylarkList.class, |
Laszlo Csomor | aded1c2 | 2015-02-16 16:57:12 +0000 | [diff] [blame^] | 805 | doc = "Returns a list strings: the names of the fields and " |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 806 | + "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 Rideau | cbebd63 | 2015-02-11 16:56:37 +0000 | [diff] [blame] | 838 | return EvalUtils.getDataTypeName(args[0]); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 839 | } |
| 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 Csomor | aded1c2 | 2015-02-16 16:57:12 +0000 | [diff] [blame^] | 914 | + "x = []\n" |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 915 | + "for s in \"abc\":\n" |
Laszlo Csomor | aded1c2 | 2015-02-16 16:57:12 +0000 | [diff] [blame^] | 916 | + " x += [s] # x == [\"a\", \"b\", \"c\"]</pre>") |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 917 | 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 | } |