// 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.io.Serializable; 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 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 readGlobalVariables = new HashSet<>(); private ImmutableList 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 stackTrace = new ImmutableList.Builder() .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 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 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 entry : env.entrySet()) { newEnv.env.put(entry.getKey(), entry.getValue()); } for (Map.Entry, Map> 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 lookup(String), 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; } /** * 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 objectsToRemove = new ArrayList<>(); List> modulesToRemove = new ArrayList<>(); for (Map.Entry 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(); } }