// Copyright 2014 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.syntax;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* The BUILD environment.
*/
public class Environment {
@SkylarkSignature(name = "True", returnType = Boolean.class,
doc = "Literal for the boolean true.")
private static final Boolean TRUE = true;
@SkylarkSignature(name = "False", returnType = Boolean.class,
doc = "Literal for the boolean false.")
private static final Boolean FALSE = false;
@SkylarkSignature(name = "PACKAGE_NAME", returnType = String.class,
doc = "The name of the package the rule or build extension is called from. "
+ "For example, in the BUILD file some/package/BUILD
, its value "
+ "will be some/package
. "
+ "This variable is special, because its value comes from outside of the extension "
+ "module (it comes from the BUILD file), so it can only be accessed in functions "
+ "(transitively) called from BUILD files. For example:
"
+ "
def extension():\n"
+ " return PACKAGE_NAME
"
+ "In this case calling extension()
works from the BUILD file (if the "
+ "function is loaded), but not as a top level function call in the extension module.")
public static final String PKG_NAME = "PACKAGE_NAME";
/**
* There should be only one instance of this type to allow "== None" tests.
*/
@Immutable
public static final class NoneType {
@Override
public String toString() { return "None"; }
private NoneType() {}
}
@SkylarkSignature(name = "None", returnType = NoneType.class, doc = "Literal for the None value.")
public static final NoneType NONE = new NoneType();
protected final Map env = new HashMap<>();
// BaseFunctions with namespaces. Works only in the global environment.
protected final Map, Map> functions = new HashMap<>();
/**
* The parent environment. For Skylark it's the global environment,
* used for global read only variable lookup.
*/
protected final Environment parent;
/**
* Map from a Skylark extension to an environment, which contains all symbols defined in the
* extension.
*/
protected Map importedExtensions;
/**
* A set of disable variables propagating through function calling. This is needed because
* UserDefinedFunctions lock the definition Environment which should be immutable.
*/
protected Set disabledVariables = new HashSet<>();
/**
* A set of disable namespaces propagating through function calling. See disabledVariables.
*/
protected Set> disabledNameSpaces = new HashSet<>();
/**
* A set of variables propagating through function calling. It's only used to call
* native rules from Skylark build extensions.
*/
protected Set propagatingVariables = new HashSet<>();
/**
* An EventHandler for errors and warnings. This is not used in the BUILD language,
* however it might be used in Skylark code called from the BUILD language.
*/
@Nullable protected EventHandler eventHandler;
/**
* Constructs an empty root non-Skylark environment.
* The root environment is also the global environment.
*/
public Environment() {
this.parent = null;
this.importedExtensions = new HashMap<>();
setupGlobal();
}
/**
* Constructs an empty child environment.
*/
public Environment(Environment parent) {
Preconditions.checkNotNull(parent);
this.parent = parent;
this.importedExtensions = new HashMap<>();
}
/**
* Constructs an empty child environment with an EventHandler.
*/
public Environment(Environment parent, EventHandler eventHandler) {
this(parent);
this.eventHandler = Preconditions.checkNotNull(eventHandler);
}
public EventHandler getEventHandler() {
return eventHandler;
}
// Sets up the global environment
private void setupGlobal() {
// In Python 2.x, True and False are global values and can be redefined by the user.
// In Python 3.x, they are keywords. We implement them as values, for the sake of
// simplicity. We define them as Boolean objects.
update("False", FALSE);
update("True", TRUE);
update("None", NONE);
}
public boolean isSkylarkEnabled() {
return false;
}
protected boolean hasVariable(String varname) {
return env.containsKey(varname);
}
/**
* @return the value from the environment whose name is "varname".
* @throws NoSuchVariableException if the variable is not defined in the Environment.
*
*/
public Object lookup(String varname) throws NoSuchVariableException {
if (disabledVariables.contains(varname)) {
throw new NoSuchVariableException(varname);
}
Object value = env.get(varname);
if (value == null) {
if (parent != null) {
return parent.lookup(varname);
}
throw new NoSuchVariableException(varname);
}
return value;
}
/**
* Like lookup(String)
, but instead of throwing an exception in
* the case where "varname" is not defined, "defaultValue" is returned instead.
*
*/
public Object lookup(String varname, Object defaultValue) {
Object value = env.get(varname);
if (value == null) {
if (parent != null) {
return parent.lookup(varname, defaultValue);
}
return defaultValue;
}
return value;
}
/**
* Updates the value of variable "varname" in the environment, corresponding
* to an {@link AssignmentStatement}.
*/
public void update(String varname, Object value) {
Preconditions.checkNotNull(value, "update(value == null)");
env.put(varname, value);
}
/**
* Same as {@link #update}, but also marks the variable propagating, meaning it will
* be present in the execution environment of a UserDefinedFunction called from this
* Environment. Using this method is discouraged.
*/
public void updateAndPropagate(String varname, Object value) {
update(varname, value);
propagatingVariables.add(varname);
}
/**
* Remove the variable from the environment, returning
* any previous mapping (null if there was none).
*/
public Object remove(String varname) {
return env.remove(varname);
}
/**
* Returns the (immutable) set of names of all variables directly defined in this environment.
*/
public Set getDirectVariableNames() {
return env.keySet();
}
/**
* Returns the (immutable) set of names of all variables defined in this
* environment. Exposed for testing; not very efficient!
*/
@VisibleForTesting
public Set getVariableNames() {
if (parent == null) {
return env.keySet();
} else {
Set vars = new HashSet<>();
vars.addAll(env.keySet());
vars.addAll(parent.getVariableNames());
return vars;
}
}
@Override
public int hashCode() {
throw new UnsupportedOperationException(); // avoid nondeterminism
}
@Override
public boolean equals(Object that) {
throw new UnsupportedOperationException();
}
@Override
public String toString() {
StringBuilder out = new StringBuilder();
out.append("Environment{");
List keys = new ArrayList<>(env.keySet());
Collections.sort(keys);
for (String key : keys) {
out.append(key).append(" -> ").append(env.get(key)).append(", ");
}
out.append("}");
if (parent != null) {
out.append("=>");
out.append(parent);
}
return out.toString();
}
/**
* An exception thrown when an attempt is made to lookup a non-existent
* variable in the environment.
*/
public static class NoSuchVariableException extends Exception {
NoSuchVariableException(String variable) {
super("no such variable: " + variable);
}
}
/**
* An exception thrown when an attempt is made to import a symbol from a file
* that was not properly loaded.
*/
public static class LoadFailedException extends Exception {
LoadFailedException(String file) {
super("file '" + file + "' was not correctly loaded. Make sure the 'load' statement appears "
+ "in the global scope, in the BUILD file");
}
}
public void setImportedExtensions(Map importedExtensions) {
this.importedExtensions = importedExtensions;
}
public void importSymbol(PathFragment extension, Identifier symbol, String nameInLoadedFile)
throws NoSuchVariableException, LoadFailedException {
if (!importedExtensions.containsKey(extension)) {
throw new LoadFailedException(extension.toString());
}
Object value = importedExtensions.get(extension).lookup(nameInLoadedFile);
if (!isSkylarkEnabled()) {
value = SkylarkType.convertFromSkylark(value);
}
update(symbol.getName(), value);
}
/**
* Registers a function with namespace to this global environment.
*/
public void registerFunction(Class> nameSpace, String name, BaseFunction function) {
nameSpace = getCanonicalRepresentation(nameSpace);
Preconditions.checkArgument(parent == null);
if (!functions.containsKey(nameSpace)) {
functions.put(nameSpace, new HashMap());
}
functions.get(nameSpace).put(name, function);
}
private Map getNamespaceFunctions(Class> nameSpace) {
nameSpace = getCanonicalRepresentation(nameSpace);
if (disabledNameSpaces.contains(nameSpace)
|| (parent != null && parent.disabledNameSpaces.contains(nameSpace))) {
return null;
}
Environment topLevel = this;
while (topLevel.parent != null) {
topLevel = topLevel.parent;
}
return topLevel.functions.get(nameSpace);
}
/**
* Returns the canonical representation of the given class, i.e. the super class for which any
* functions were registered.
*
* Currently, this is only necessary for mapping the different subclasses of {@link
* java.util.Map} to the interface.
*/
private Class> getCanonicalRepresentation(Class> clazz) {
return Map.class.isAssignableFrom(clazz) ? Map.class : clazz;
}
/**
* Returns the function of the namespace of the given name or null of it does not exists.
*/
public BaseFunction getFunction(Class> nameSpace, String name) {
Map nameSpaceFunctions = getNamespaceFunctions(nameSpace);
return nameSpaceFunctions != null ? nameSpaceFunctions.get(name) : null;
}
/**
* Returns the function names registered with the namespace.
*/
public Set getFunctionNames(Class> nameSpace) {
Map nameSpaceFunctions = getNamespaceFunctions(nameSpace);
return nameSpaceFunctions != null ? nameSpaceFunctions.keySet() : ImmutableSet.of();
}
/**
* Return the current stack trace (list of function names).
*/
public ImmutableList getStackTrace() {
// Empty list, since this environment does not allow function definition
// (see SkylarkEnvironment)
return ImmutableList.of();
}
}