blob: f84c6ee664b718e039297d9d1b64089ea804a923 [file] [log] [blame]
// 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.Iterables;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.util.Fingerprint;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.Nullable;
/**
* The environment for Skylark.
*/
public class SkylarkEnvironment extends Environment {
/**
* This set contains the variable names of all the successful lookups from the global
* environment. This is necessary because if in a function definition something
* reads a global variable after which a local variable with the same name is assigned an
* Exception needs to be thrown.
*/
private final Set<String> readGlobalVariables = new HashSet<>();
private ImmutableList<String> stackTrace;
@Nullable private String fileContentHashCode;
/**
* Creates a Skylark Environment for function calling, from the global Environment of the
* caller Environment (which must be a Skylark Environment).
*/
public static SkylarkEnvironment createEnvironmentForFunctionCalling(
Environment callerEnv, SkylarkEnvironment definitionEnv,
UserDefinedFunction function) throws EvalException {
if (callerEnv.getStackTrace().contains(function.getName())) {
throw new EvalException(function.getLocation(), "Recursion was detected when calling '"
+ function.getName() + "' from '" + Iterables.getLast(callerEnv.getStackTrace()) + "'");
}
ImmutableList<String> stackTrace = new ImmutableList.Builder<String>()
.addAll(callerEnv.getStackTrace())
.add(function.getName())
.build();
SkylarkEnvironment childEnv =
// Always use the caller Environment's EventHandler. We cannot assume that the
// definition Environment's EventHandler is still working properly.
new SkylarkEnvironment(definitionEnv, stackTrace, callerEnv.eventHandler);
try {
for (String varname : callerEnv.propagatingVariables) {
childEnv.updateAndPropagate(varname, callerEnv.lookup(varname));
}
} catch (NoSuchVariableException e) {
// This should never happen.
throw new IllegalStateException(e);
}
childEnv.disabledVariables = callerEnv.disabledVariables;
childEnv.disabledNameSpaces = callerEnv.disabledNameSpaces;
return childEnv;
}
private SkylarkEnvironment(SkylarkEnvironment definitionEnv, ImmutableList<String> stackTrace,
EventHandler eventHandler) {
super(definitionEnv.getGlobalEnvironment());
this.stackTrace = stackTrace;
this.eventHandler = Preconditions.checkNotNull(eventHandler,
"EventHandler cannot be null in an Environment which calls into Skylark");
}
/**
* Creates a global SkylarkEnvironment.
*/
public SkylarkEnvironment(EventHandler eventHandler, String astFileContentHashCode) {
super();
stackTrace = ImmutableList.of();
this.eventHandler = eventHandler;
this.fileContentHashCode = astFileContentHashCode;
}
@VisibleForTesting
public SkylarkEnvironment(EventHandler eventHandler) {
this(eventHandler, null);
}
public SkylarkEnvironment(SkylarkEnvironment globalEnv) {
super(globalEnv);
stackTrace = ImmutableList.of();
this.eventHandler = globalEnv.eventHandler;
}
@Override
public ImmutableList<String> getStackTrace() {
return stackTrace;
}
/**
* Clones this Skylark global environment.
*/
public SkylarkEnvironment cloneEnv(EventHandler eventHandler) {
Preconditions.checkArgument(isGlobalEnvironment());
SkylarkEnvironment newEnv = new SkylarkEnvironment(eventHandler, this.fileContentHashCode);
for (Entry<String, Object> entry : env.entrySet()) {
newEnv.env.put(entry.getKey(), entry.getValue());
}
for (Map.Entry<Class<?>, Map<String, Function>> functionMap : functions.entrySet()) {
newEnv.functions.put(functionMap.getKey(), functionMap.getValue());
}
return newEnv;
}
/**
* Returns the global environment. Only works for Skylark environments. For the global Skylark
* environment this method returns this Environment.
*/
public SkylarkEnvironment getGlobalEnvironment() {
// If there's a parent that's the global environment, otherwise this is.
return parent != null ? (SkylarkEnvironment) parent : this;
}
/**
* Returns true if this is a Skylark global environment.
*/
public boolean isGlobalEnvironment() {
return parent == null;
}
/**
* Returns true if varname has been read as a global variable.
*/
public boolean hasBeenReadGlobalVariable(String varname) {
return readGlobalVariables.contains(varname);
}
@Override
public boolean isSkylarkEnabled() {
return true;
}
/**
* @return the value from the environment whose name is "varname".
* @throws NoSuchVariableException if the variable is not defined in the environment.
*/
@Override
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 && parent.hasVariable(varname)) {
readGlobalVariables.add(varname);
return parent.lookup(varname);
}
throw new NoSuchVariableException(varname);
}
return value;
}
/**
* Like <code>lookup(String)</code>, but instead of throwing an exception in
* the case where "varname" is not defined, "defaultValue" is returned instead.
*/
@Override
public Object lookup(String varname, Object defaultValue) {
throw new UnsupportedOperationException();
}
/**
* Updates the value of variable "varname" in the environment, corresponding
* to an AssignmentStatement.
*/
@Override
public void update(String varname, Object value) {
Preconditions.checkNotNull(value, "update(value == null)");
env.put(varname, value);
}
/**
* Returns the class of the variable or null if the variable does not exist. This function
* works only in the local Environment, it doesn't check the global Environment.
*/
public Class<?> getVariableType(String varname) {
Object variable = env.get(varname);
return variable != null ? EvalUtils.getSkylarkType(variable.getClass()) : null;
}
/**
* Removes the functions and the modules (i.e. the symbol of the module from the top level
* Environment and the functions attached to it) from the Environment which should be present
* only during the loading phase.
*/
public void disableOnlyLoadingPhaseObjects() {
List<String> objectsToRemove = new ArrayList<>();
List<Class<?>> modulesToRemove = new ArrayList<>();
for (Map.Entry<String, Object> entry : env.entrySet()) {
Object object = entry.getValue();
if (object instanceof SkylarkFunction) {
if (((SkylarkFunction) object).isOnlyLoadingPhase()) {
objectsToRemove.add(entry.getKey());
}
} else if (object.getClass().isAnnotationPresent(SkylarkModule.class)) {
if (object.getClass().getAnnotation(SkylarkModule.class).onlyLoadingPhase()) {
objectsToRemove.add(entry.getKey());
modulesToRemove.add(entry.getValue().getClass());
}
}
}
disabledVariables.addAll(objectsToRemove);
disabledNameSpaces.addAll(modulesToRemove);
}
public void handleEvent(Event event) {
eventHandler.handle(event);
}
/**
* Returns a hash code calculated from the hash code of this Environment and the
* transitive closure of other Environments it loads.
*/
public String getTransitiveFileContentHashCode() {
Fingerprint fingerprint = new Fingerprint();
fingerprint.addString(Preconditions.checkNotNull(fileContentHashCode));
// Calculate a new hash from the hash of the loaded Environments.
for (SkylarkEnvironment env : importedExtensions.values()) {
fingerprint.addString(env.getTransitiveFileContentHashCode());
}
return fingerprint.hexDigestAndReset();
}
}