blob: 2246e53c45350b87ad310bb868ca061a071f64f2 [file] [log] [blame]
// Copyright 2018 The Bazel Authors. 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.packages;
import com.google.common.base.Preconditions;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkThread;
/*
* TODO(b/236456122): We should break this class up into separate classes for each kind of Starlark
* evaluation environment, as opposed to storing possibly inapplicable nullable fields on this
* class. Then we can use static methods with signatures like fromOrFail(StarlarkThread, String) in
* place of the Phase enum and its associated check* methods.
*
* Some kinds of evaluation environments include:
* - .bzl loading
* - BUILD evaluation (should absorb PackageFactory.PackageContext -- no need to store multiple
* thread-locals in StarlarkThread)
* - WORKSPACE evaluation (shares logic with BUILD)
* - implicit outputs
* - computed defaults
* - transition implementation
* - Args.map_each
* - probably others
*
* BazelStarlarkContext itself could probably be deleted. The only thing common to almost all Bazel
* Starlark environments is SymbolGenerator, which in principle could be used to uniquely identify
* depsets or even StarlarkFunction, though in practice it doesn't have nearly that widespread usage
* yet. SymbolGenerator could be promoted to a core feature of StarlarkThread if it reaches that
* point. If we do keep BazelStarlarkContext around as a common base class of the other context
* classes, it should be renamed BazelThreadContext for symmetry with BazelModuleContext.
*
* Even if we can otherwise get rid of BazelStarlarkContext, it may still be handy to retain this
* class as a static namespace for helper methods for storing and retrieving contexts on
* StarlarkThreads. In particular, it'd avoid the very likely bug of a storeInThread
* implementation in one of the context classes forgetting to setUncheckedExceptionContext(this). It
* also would give us a place to keep javadoc about the proper way to use these context objects in
* Bazel.
*/
/**
* Bazel-specific contextual information associated with a Starlark evaluation thread.
*
* <p>This is stored in the StarlarkThread object as a thread-local. A subclass of this class may be
* used for certain kinds of Starlark evaluations; in that case it is still keyed in the
* thread-locals under {@code BazelStarlarkContext.class}.
*
* <p>This object is mutable and should not be accessed simultaneously or reused for more than one
* Starlark thread.
*/
public class BazelStarlarkContext implements StarlarkThread.UncheckedExceptionContext {
/** The phase to which this Starlark thread belongs. */
// TODO(b/236456122): Eliminate.
public enum Phase {
WORKSPACE,
LOADING,
ANALYSIS
}
/**
* Retrieves this context from a Starlark thread, or throws {@link EvalException} if unavailable.
*/
public static BazelStarlarkContext fromOrFail(StarlarkThread thread) throws EvalException {
BazelStarlarkContext ctx = thread.getThreadLocal(BazelStarlarkContext.class);
if (ctx == null) {
throw Starlark.errorf(
"this function cannot be called from %s", thread.getContextDescription());
}
return ctx;
}
/**
* Saves this {@code BazelStarlarkContext} in the specified Starlark thread. Call only once,
* before evaluation begins.
*/
public void storeInThread(StarlarkThread thread) {
Preconditions.checkState(thread.getThreadLocal(BazelStarlarkContext.class) == null);
thread.setThreadLocal(BazelStarlarkContext.class, this);
// TODO(b/236456122): We can probably replace the concept of setUncheckedExceptionContext with a
// string parameter to StarlarkThread construction. In that case, storeInThread() becomes more
// superfluous, and there's one less reason to keep the inheritance hierarchy of
// BazelStarlarkContext and its children.
thread.setUncheckedExceptionContext(this);
}
// A generic counter for uniquely identifying symbols created in this Starlark evaluation.
private final SymbolGenerator<?> symbolGenerator;
// TODO(b/236456122): Eliminate Phase, migrate analysisRuleLabel to a separate context class.
private final Phase phase;
/**
* @param phase the phase to which this Starlark thread belongs
* @param symbolGenerator a {@link SymbolGenerator} to be used when creating objects to be
* compared using reference equality.
*/
// TODO(b/236456122): Consider taking an owner in place of a SymbolGenerator, and constructing
// the latter ourselves. Seems like we don't want to tempt anyone into sharing a SymbolGenerator.
public BazelStarlarkContext(Phase phase, SymbolGenerator<?> symbolGenerator) {
this.phase = Preconditions.checkNotNull(phase);
this.symbolGenerator = Preconditions.checkNotNull(symbolGenerator);
}
/** Returns the phase associated with this context. */
public Phase getPhase() {
return phase;
}
public SymbolGenerator<?> getSymbolGenerator() {
return symbolGenerator;
}
@Override
public String getContextForUncheckedException() {
return phase.toString();
}
/**
* Checks that the Starlark thread is in the loading or the workspace phase.
*
* @param function name of a function that requires this check
*/
// TODO(b/236456122): The Phase enum is incomplete. Ex: `Args.map_each` evaluation happens at
// execution time. So this is a misnomer and possibly wrong in those contexts.
public static void checkLoadingOrWorkspacePhase(StarlarkThread thread, String function)
throws EvalException {
BazelStarlarkContext ctx = thread.getThreadLocal(BazelStarlarkContext.class);
if (ctx == null) {
throw Starlark.errorf(
"'%s' cannot be called from %s", function, thread.getContextDescription());
}
if (ctx.phase == Phase.ANALYSIS) {
throw Starlark.errorf("'%s' cannot be called during the analysis phase", function);
}
}
/**
* Checks that the current StarlarkThread is in the loading phase.
*
* @param function name of a function that requires this check
*/
public static void checkLoadingPhase(StarlarkThread thread, String function)
throws EvalException {
BazelStarlarkContext ctx = thread.getThreadLocal(BazelStarlarkContext.class);
if (ctx == null) {
throw Starlark.errorf(
"'%s' cannot be called from %s", function, thread.getContextDescription());
}
if (ctx.phase != Phase.LOADING) {
throw Starlark.errorf(
"'%s' can only be called from a BUILD file, or a macro invoked from a BUILD file",
function);
}
}
/**
* Checks that the current StarlarkThread is in the workspace phase.
*
* @param function name of a function that requires this check
*/
public static void checkWorkspacePhase(StarlarkThread thread, String function)
throws EvalException {
BazelStarlarkContext ctx = thread.getThreadLocal(BazelStarlarkContext.class);
if (ctx == null) {
throw Starlark.errorf(
"'%s' cannot be called from %s", function, thread.getContextDescription());
}
if (ctx.phase != Phase.WORKSPACE) {
throw Starlark.errorf("'%s' can only be called during workspace loading", function);
}
}
}