Skylark error messages now include a stack trace.
This means that some tests had to be changed from using exact equality of error messages to working with contains() / startsWith().
--
MOS_MIGRATED_REVID=100923593
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/BaseFunction.java b/src/main/java/com/google/devtools/build/lib/syntax/BaseFunction.java
index 5145f4b..0309af2 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/BaseFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/BaseFunction.java
@@ -396,21 +396,31 @@
}
/**
+ * Returns the environment for the scope of this function.
+ *
+ * <p>Since this is a BaseFunction, we don't create a new environment.
+ */
+ @SuppressWarnings("unused") // For the exception
+ protected Environment getOrCreateChildEnvironment(Environment parent) throws EvalException {
+ return parent;
+ }
+
+ /**
* The outer calling convention to a BaseFunction.
*
* @param args a list of all positional arguments (as in *starArg)
* @param kwargs a map for key arguments (as in **kwArgs)
* @param ast the expression for this function's definition
- * @param env the lexical Environment for the function call
+ * @param parentEnv the lexical Environment for the function call
* @return the value resulting from evaluating the function with the given arguments
* @throws construction of EvalException-s containing source information.
*/
public Object call(@Nullable List<Object> args,
@Nullable Map<String, Object> kwargs,
@Nullable FuncallExpression ast,
- @Nullable Environment env)
+ @Nullable Environment parentEnv)
throws EvalException, InterruptedException {
-
+ Environment env = getOrCreateChildEnvironment(parentEnv);
Preconditions.checkState(isConfigured(), "Function %s was not configured", getName());
// ast is null when called from Java (as there's no Skylark call site).
@@ -421,24 +431,37 @@
try {
return call(arguments, ast, env);
- } catch (ConversionException e) {
- throw new EvalException(loc, e.getMessage());
+ } catch (EvalExceptionWithStackTrace ex) {
+ throw updateStackTrace(ex, loc);
+ } catch (EvalException | RuntimeException | InterruptedException ex) {
+ throw updateStackTrace(new EvalExceptionWithStackTrace(ex, Location.BUILTIN), loc);
}
}
/**
+ * Adds an entry for the current function to the stack trace of the exception.
+ */
+ private EvalExceptionWithStackTrace updateStackTrace(
+ EvalExceptionWithStackTrace ex, Location location) {
+ ex.registerFunction(this);
+ ex.setLocation(location);
+ return ex;
+ }
+
+ /**
* Inner call to a BaseFunction
* subclasses need to @Override this method.
*
* @param args an array of argument values sorted as per the signature.
* @param ast the source code for the function if user-defined
- * @param ast the lexical environment of the function call
+ * @param env the lexical environment of the function call
*/
// Don't make it abstract, so that subclasses may be defined that @Override the outer call() only.
- public Object call(Object[] args,
+ protected Object call(Object[] args,
@Nullable FuncallExpression ast, @Nullable Environment env)
throws EvalException, ConversionException, InterruptedException {
- throw new EvalException(ast.getLocation(),
+ throw new EvalException(
+ (ast == null) ? Location.BUILTIN : ast.getLocation(),
String.format("function %s not implemented", getName()));
}
@@ -498,14 +521,22 @@
}
/**
- * Returns the signature as "[className.]methodName(paramType1, paramType2, ...)"
+ * Returns [class.]function (depending on whether func belongs to a class).
*/
- public String getShortSignature() {
+ public String getFullName() {
+ return String.format("%s%s", getObjectTypeString(), getName());
+ }
+
+ /**
+ * Returns the signature as "[className.]methodName(name1: paramType1, name2: paramType2, ...)"
+ * or "[className.]methodName(paramType1, paramType2, ...)", depending on the value of showNames.
+ */
+ public String getShortSignature(boolean showNames) {
StringBuilder builder = new StringBuilder();
boolean hasSelf = hasSelfArgument();
- builder.append(getObjectTypeString()).append(getName()).append("(");
- signature.toStringBuilder(builder, true, false, false, hasSelf);
+ builder.append(getFullName()).append("(");
+ signature.toStringBuilder(builder, showNames, false, false, hasSelf);
builder.append(")");
return builder.toString();
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/BuildFileAST.java b/src/main/java/com/google/devtools/build/lib/syntax/BuildFileAST.java
index e8f901b..e2a333a 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/BuildFileAST.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/BuildFileAST.java
@@ -190,12 +190,9 @@
// BUILD file (as it is the most probable cause for the error).
Location exnLoc = e.getLocation();
Location nodeLoc = stmt.getLocation();
- if (exnLoc == null || !nodeLoc.getPath().equals(exnLoc.getPath())) {
- eventHandler.handle(Event.error(nodeLoc,
- e.getMessage() + " (raised from " + exnLoc + ")"));
- } else {
- eventHandler.handle(Event.error(exnLoc, e.getMessage()));
- }
+ eventHandler.handle(Event.error(
+ (exnLoc == null || !nodeLoc.getPath().equals(exnLoc.getPath())) ? nodeLoc : exnLoc,
+ e.getMessage()));
}
}
return ok;
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/BuiltinFunction.java b/src/main/java/com/google/devtools/build/lib/syntax/BuiltinFunction.java
index 7fc85ba..70173ce 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/BuiltinFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/BuiltinFunction.java
@@ -177,7 +177,7 @@
loc,
String.format(
"Method %s is not applicable for arguments %s: '%s' is %s, but should be %s",
- getShortSignature(), printTypeString(args, args.length - extraArgsCount),
+ getShortSignature(true), printTypeString(args, args.length - extraArgsCount),
paramName, EvalUtils.getDataTypeName(args[i]),
EvalUtils.getDataTypeNameFromClass(types[i])));
}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/DotExpression.java b/src/main/java/com/google/devtools/build/lib/syntax/DotExpression.java
index 1b98a27..e6ae8a0 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/DotExpression.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/DotExpression.java
@@ -70,8 +70,12 @@
*/
public static Object eval(Object objValue, String name, Location loc) throws EvalException {
if (objValue instanceof ClassObject) {
- Object result = ((ClassObject) objValue).getValue(name);
-
+ Object result = null;
+ try {
+ result = ((ClassObject) objValue).getValue(name);
+ } catch (IllegalArgumentException ex) {
+ throw new EvalException(loc, ex);
+ }
// ClassObjects may have fields that are annotated with @SkylarkCallable.
// Since getValue() does not know about those, we cannot expect that result is a valid object.
if (result != null) {
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/EvalException.java b/src/main/java/com/google/devtools/build/lib/syntax/EvalException.java
index 361ce9f..32f289b 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/EvalException.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/EvalException.java
@@ -70,7 +70,7 @@
if (message == null) {
message = "";
}
- if (cause != null) {
+ if (cause != null && !message.contains(cause.getMessage())) {
message = message + (message.isEmpty() ? "" : ": ") + cause.getMessage();
}
if (message.isEmpty()) {
@@ -92,7 +92,16 @@
return (getLocation() == null ? "" : getLocation()) + ": "
+ (message == null ? "" : message + "\n")
+ (dueToIncompleteAST ? "due to incomplete AST\n" : "")
- + (getCause() != null && getCause().getMessage() != null ? getCause().getMessage() : "");
+ + getCauseMessage();
+ }
+
+ private String getCauseMessage() {
+ Throwable cause = getCause();
+ if (cause == null) {
+ return "";
+ }
+ String causeMessage = cause.getMessage();
+ return (causeMessage == null || message.contains(causeMessage)) ? "" : causeMessage;
}
/**
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/EvalExceptionWithStackTrace.java b/src/main/java/com/google/devtools/build/lib/syntax/EvalExceptionWithStackTrace.java
new file mode 100644
index 0000000..5047521
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/EvalExceptionWithStackTrace.java
@@ -0,0 +1,82 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.syntax;
+
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.Rule;
+
+/**
+ * EvalException with a stack trace
+ */
+public class EvalExceptionWithStackTrace extends EvalException {
+ private final StringBuilder builder = new StringBuilder();
+ private Location location;
+ private boolean printStackTrace;
+
+ public EvalExceptionWithStackTrace(Exception original, Location callLocation) {
+ super(callLocation, original.getMessage(), original.getCause());
+ setLocation(callLocation);
+ builder.append(super.getMessage());
+ printStackTrace = false;
+ }
+
+ /**
+ * Adds a line for the given function to the stack trace. Requires that #setLocation() was called
+ * previously.
+ */
+ public void registerFunction(BaseFunction function) {
+ addStackFrame(function.getFullName());
+ }
+
+ /**
+ * Adds a line for the given rule to the stack trace.
+ */
+ public void registerRule(Rule rule) {
+ setLocation(rule.getLocation());
+ addStackFrame(String.format("%s(name = '%s', ...)", rule.getRuleClass(), rule.getName()));
+ }
+
+ /**
+ * Adds a line for the given scope (function or rule).
+ */
+ private void addStackFrame(String scope) {
+ builder.append(String.format("\n\tin %s [%s]", scope, location));
+ printStackTrace |= (location != Location.BUILTIN);
+ }
+
+ /**
+ * Sets the location for the next function to be added via #registerFunction().
+ */
+ public void setLocation(Location callLocation) {
+ this.location = callLocation;
+ }
+
+ /**
+ * Returns the exception message without the stack trace.
+ */
+ public String getOriginalMessage() {
+ return super.getMessage();
+ }
+
+ @Override
+ public String getMessage() {
+ return print();
+ }
+
+ @Override
+ public String print() {
+ // Only print the stack trace when it contains more than one built-in function.
+ return printStackTrace ? builder.toString() : getOriginalMessage();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java b/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java
index a448325..32fda83 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java
@@ -481,64 +481,91 @@
@Override
Object eval(Environment env) throws EvalException, InterruptedException {
+ try {
+ return (obj != null) ? invokeObjectMethod(env) : invokeGlobalFunction(env);
+ } catch (EvalException ex) {
+ // BaseFunction will not get the exact location of some errors (such as exceptions thrown in
+ // #invokeJavaMethod), so we create a new stack trace with the correct location here.
+ throw (ex instanceof EvalExceptionWithStackTrace)
+ ? ex
+ : new EvalExceptionWithStackTrace(ex, ex.getLocation());
+ }
+ }
+
+ /**
+ * Invokes obj.func() and returns the result.
+ */
+ private Object invokeObjectMethod(Environment env) throws EvalException, InterruptedException {
+ Object objValue = obj.eval(env);
ImmutableList.Builder<Object> posargs = new ImmutableList.Builder<>();
// We copy this into an ImmutableMap in the end, but we can't use an ImmutableMap.Builder, or
// we'd still have to have a HashMap on the side for the sake of properly handling duplicates.
Map<String, Object> kwargs = new HashMap<>();
- Object returnValue;
- BaseFunction function;
- if (obj != null) { // obj.func(...)
- Object objValue = obj.eval(env);
- // Strings, lists and dictionaries (maps) have functions that we want to use in MethodLibrary.
- // For other classes, we can call the Java methods.
- function =
- env.getFunction(EvalUtils.getSkylarkType(objValue.getClass()), func.getName());
- if (function != null) {
- if (!isNamespace(objValue.getClass())) {
- // Add self as an implicit parameter in front.
- posargs.add(objValue);
- }
- evalArguments(posargs, kwargs, env, function);
- returnValue = function.call(
- posargs.build(), ImmutableMap.<String, Object>copyOf(kwargs), this, env);
- } else if (env.isSkylarkEnabled()) {
- // Only allow native Java calls when using Skylark
- // When calling a Java method, the name is not in the Environment,
- // so evaluating 'func' would fail.
- evalArguments(posargs, kwargs, env, null);
- if (!kwargs.isEmpty()) {
- throw new EvalException(func.getLocation(),
- String.format("Keyword arguments are not allowed when calling a java method"
- + "\nwhile calling method '%s' on object %s of type %s",
- func.getName(), objValue, EvalUtils.getDataTypeName(objValue)));
- }
- if (objValue instanceof Class<?>) {
- // Static Java method call. We can return the value from here directly because
- // invokeJavaMethod() has special checks.
- return invokeJavaMethod(null, (Class<?>) objValue, func.getName(), posargs.build());
- } else {
- return invokeJavaMethod(objValue, objValue.getClass(), func.getName(), posargs.build());
- }
- } else {
- throw new EvalException(getLocation(), String.format(
- "%s is not defined on object of type '%s'",
- functionName(), EvalUtils.getDataTypeName(objValue)));
+ // Strings, lists and dictionaries (maps) have functions that we want to use in
+ // MethodLibrary.
+ // For other classes, we can call the Java methods.
+ BaseFunction function =
+ env.getFunction(EvalUtils.getSkylarkType(objValue.getClass()), func.getName());
+ if (function != null) {
+ if (!isNamespace(objValue.getClass())) {
+ // Add self as an implicit parameter in front.
+ posargs.add(objValue);
}
- } else { // func(...)
- Object funcValue = func.eval(env);
- if ((funcValue instanceof BaseFunction)) {
- function = (BaseFunction) funcValue;
- evalArguments(posargs, kwargs, env, function);
- returnValue = function.call(
- posargs.build(), ImmutableMap.<String, Object>copyOf(kwargs), this, env);
- } else {
- throw new EvalException(getLocation(),
- "'" + EvalUtils.getDataTypeName(funcValue)
- + "' object is not callable");
+ evalArguments(posargs, kwargs, env, function);
+ return convertFromSkylark(
+ function.call(posargs.build(), ImmutableMap.<String, Object>copyOf(kwargs), this, env),
+ env);
+ } else if (env.isSkylarkEnabled()) {
+ // Only allow native Java calls when using Skylark
+ // When calling a Java method, the name is not in the Environment,
+ // so evaluating 'func' would fail.
+ evalArguments(posargs, kwargs, env, null);
+ if (!kwargs.isEmpty()) {
+ throw new EvalException(
+ func.getLocation(),
+ String.format(
+ "Keyword arguments are not allowed when calling a java method"
+ + "\nwhile calling method '%s' on object %s of type %s",
+ func.getName(), objValue, EvalUtils.getDataTypeName(objValue)));
}
+ if (objValue instanceof Class<?>) {
+ // Static Java method call. We can return the value from here directly because
+ // invokeJavaMethod() has special checks.
+ return invokeJavaMethod(null, (Class<?>) objValue, func.getName(), posargs.build());
+ } else {
+ return invokeJavaMethod(objValue, objValue.getClass(), func.getName(), posargs.build());
+ }
+ } else {
+ throw new EvalException(
+ getLocation(),
+ String.format("%s is not defined on object of type '%s'", functionName(),
+ EvalUtils.getDataTypeName(objValue)));
}
+ }
+ /**
+ * Invokes func() and returns the result.
+ */
+ private Object invokeGlobalFunction(Environment env) throws EvalException, InterruptedException {
+ Object funcValue = func.eval(env);
+ ImmutableList.Builder<Object> posargs = new ImmutableList.Builder<>();
+ // We copy this into an ImmutableMap in the end, but we can't use an ImmutableMap.Builder, or
+ // we'd still have to have a HashMap on the side for the sake of properly handling duplicates.
+ Map<String, Object> kwargs = new HashMap<>();
+ if ((funcValue instanceof BaseFunction)) {
+ BaseFunction function = (BaseFunction) funcValue;
+ evalArguments(posargs, kwargs, env, function);
+ return convertFromSkylark(
+ function.call(posargs.build(), ImmutableMap.<String, Object>copyOf(kwargs), this, env),
+ env);
+ } else {
+ throw new EvalException(
+ getLocation(), "'" + EvalUtils.getDataTypeName(funcValue) + "' object is not callable");
+ }
+ }
+
+ protected Object convertFromSkylark(Object returnValue, Environment env) throws EvalException {
EvalUtils.checkNotNull(this, returnValue);
if (!env.isSkylarkEnabled()) {
// The call happens in the BUILD language. Note that accessing "BUILD language" functions in
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java b/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java
index 5831a28..334a5eb 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java
@@ -46,35 +46,33 @@
return location;
}
- /**
- * Since the types of parameters of user defined functions are unknown, we just return
- * "name(parameterCount)"
- */
- @Override
- public String getShortSignature() {
- return String.format("%s(%d)", getName(), getArgArraySize());
- }
-
@Override
public Object call(Object[] arguments, FuncallExpression ast, Environment env)
throws EvalException, InterruptedException {
- SkylarkEnvironment functionEnv = SkylarkEnvironment.createEnvironmentForFunctionCalling(
- env, definitionEnv, this);
ImmutableList<String> names = signature.getSignature().getNames();
// Registering the functions's arguments as variables in the local Environment
int i = 0;
for (String name : names) {
- functionEnv.update(name, arguments[i++]);
+ env.update(name, arguments[i++]);
}
try {
for (Statement stmt : statements) {
- stmt.exec(functionEnv);
+ stmt.exec(env);
}
} catch (ReturnStatement.ReturnException e) {
return e.getValue();
}
return Environment.NONE;
}
+
+ /**
+ * Creates a new environment for the execution of this function.
+ */
+ @Override
+ protected Environment getOrCreateChildEnvironment(Environment parent) throws EvalException {
+ return SkylarkEnvironment.createEnvironmentForFunctionCalling(
+ parent, definitionEnv, this);
+ }
}