blob: 0ff19f90c4d2fdad2856292df8dd456cd3ccfc20 [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.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.io.Serializable;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.Nullable;
/**
* The environment for Skylark.
*/
public class SkylarkEnvironment extends Environment implements Serializable {
/**
* 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<>();
@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.stackTraceContains(function)) {
throw new EvalException(
function.getLocation(),
"Recursion was detected when calling '" + function.getName() + "' from '"
+ Iterables.getLast(callerEnv.getStackTrace()).getName() + "'");
}
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, callerEnv.getCopyOfStackTrace(), callerEnv.eventHandler);
if (callerEnv.isLoadingPhase()) {
childEnv.setLoadingPhase();
}
try {
for (String varname : callerEnv.propagatingVariables) {
childEnv.updateAndPropagate(varname, callerEnv.lookup(varname));
}
} catch (NoSuchVariableException e) {
// This should never happen.
throw new IllegalStateException(e);
}
return childEnv;
}
private SkylarkEnvironment(SkylarkEnvironment definitionEnv,
Deque<StackTraceElement> stackTrace, EventHandler eventHandler) {
super(definitionEnv.getGlobalEnvironment(), 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,
Deque<StackTraceElement> stackTrace) {
super(stackTrace);
this.eventHandler = eventHandler;
this.fileContentHashCode = astFileContentHashCode;
}
public SkylarkEnvironment(EventHandler eventHandler, String astFileContentHashCode) {
this(eventHandler, astFileContentHashCode, new LinkedList<StackTraceElement>());
}
@VisibleForTesting
public SkylarkEnvironment(EventHandler eventHandler) {
this(eventHandler, null);
}
public SkylarkEnvironment(SkylarkEnvironment globalEnv) {
super(globalEnv);
this.eventHandler = globalEnv.eventHandler;
}
/**
* Clones this Skylark global environment.
*/
public SkylarkEnvironment cloneEnv(EventHandler eventHandler) {
Preconditions.checkArgument(isGlobal());
SkylarkEnvironment newEnv =
new SkylarkEnvironment(eventHandler, this.fileContentHashCode, getCopyOfStackTrace());
for (Entry<String, Object> entry : env.entrySet()) {
newEnv.env.put(entry.getKey(), entry.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.
*/
@Override
public boolean isGlobal() {
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 isSkylark() {
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 {
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();
}
/**
* 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;
}
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();
}
}