com.facebook.buck.skylark.parser.SkylarkProjectBuildFileParser.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.skylark.parser.SkylarkProjectBuildFileParser.java

Source

/*
 * Copyright 2017-present Facebook, Inc.
 *
 * 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.facebook.buck.skylark.parser;

import com.facebook.buck.core.util.immutables.BuckStyleImmutable;
import com.facebook.buck.core.util.log.Logger;
import com.facebook.buck.event.BuckEventBus;
import com.facebook.buck.io.file.MorePaths;
import com.facebook.buck.parser.api.BuildFileManifest;
import com.facebook.buck.parser.api.BuildFileManifestPojoizer;
import com.facebook.buck.parser.api.PojoTransformer;
import com.facebook.buck.parser.api.ProjectBuildFileParser;
import com.facebook.buck.parser.events.ParseBuckFileEvent;
import com.facebook.buck.parser.exceptions.BuildFileParseException;
import com.facebook.buck.parser.implicit.ImplicitInclude;
import com.facebook.buck.parser.implicit.PackageImplicitIncludesFinder;
import com.facebook.buck.parser.options.ProjectBuildFileParserOptions;
import com.facebook.buck.parser.syntax.ImmutableListWithSelects;
import com.facebook.buck.parser.syntax.ImmutableSelectorValue;
import com.facebook.buck.skylark.io.GlobSpec;
import com.facebook.buck.skylark.io.GlobSpecWithResult;
import com.facebook.buck.skylark.io.Globber;
import com.facebook.buck.skylark.io.GlobberFactory;
import com.facebook.buck.skylark.io.impl.CachingGlobber;
import com.facebook.buck.skylark.packages.PackageContext;
import com.facebook.buck.skylark.parser.context.ParseContext;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Throwables;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.syntax.BuildFileAST;
import com.google.devtools.build.lib.syntax.Environment;
import com.google.devtools.build.lib.syntax.Environment.Extension;
import com.google.devtools.build.lib.syntax.Mutability;
import com.google.devtools.build.lib.syntax.ParserInputSource;
import com.google.devtools.build.lib.syntax.SkylarkImport;
import com.google.devtools.build.lib.syntax.SkylarkUtils;
import com.google.devtools.build.lib.syntax.SkylarkUtils.Phase;
import com.google.devtools.build.lib.syntax.StarlarkSemantics;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.immutables.value.Value;

/**
 * Parser for build files written using Skylark syntax.
 *
 * <p>NOTE: This parser is still a work in progress and does not support some functions provided by
 * Python DSL parser like {@code include_defs}, so use in production at your own risk.
 */
public class SkylarkProjectBuildFileParser implements ProjectBuildFileParser {

    private static final Logger LOG = Logger.get(SkylarkProjectBuildFileParser.class);

    private final FileSystem fileSystem;

    private final ProjectBuildFileParserOptions options;
    private final BuckEventBus buckEventBus;
    private final EventHandler eventHandler;
    private final BuckGlobals buckGlobals;
    private final GlobberFactory globberFactory;
    private final Cache<com.google.devtools.build.lib.vfs.Path, BuildFileAST> astCache;
    private final LoadingCache<LoadImport, ExtensionData> extensionDataCache;
    private final LoadingCache<LoadImport, IncludesData> includesDataCache;
    private final PackageImplicitIncludesFinder packageImplicitIncludeFinder;

    private SkylarkProjectBuildFileParser(ProjectBuildFileParserOptions options, BuckEventBus buckEventBus,
            FileSystem fileSystem, BuckGlobals buckGlobals, EventHandler eventHandler,
            GlobberFactory globberFactory) {
        this.options = options;
        this.buckEventBus = buckEventBus;
        this.fileSystem = fileSystem;
        this.eventHandler = eventHandler;
        this.buckGlobals = buckGlobals;
        this.globberFactory = globberFactory;

        this.astCache = CacheBuilder.newBuilder().build();

        this.extensionDataCache = CacheBuilder.newBuilder().build(new CacheLoader<LoadImport, ExtensionData>() {
            @Override
            public ExtensionData load(@Nonnull LoadImport loadImport) throws Exception {
                return loadExtension(loadImport);
            }
        });
        this.includesDataCache = CacheBuilder.newBuilder().build(new CacheLoader<LoadImport, IncludesData>() {
            @Override
            public IncludesData load(LoadImport loadImport) throws IOException, InterruptedException {
                return loadInclude(loadImport);
            }
        });

        this.packageImplicitIncludeFinder = PackageImplicitIncludesFinder
                .fromConfiguration(options.getPackageImplicitIncludes());
    }

    @VisibleForTesting
    protected SkylarkProjectBuildFileParser(SkylarkProjectBuildFileParser other) {
        this(other.options, other.buckEventBus, other.fileSystem, other.buckGlobals, other.eventHandler,
                other.globberFactory);
    }

    /** Create an instance of Skylark project build file parser using provided options. */
    public static SkylarkProjectBuildFileParser using(ProjectBuildFileParserOptions options,
            BuckEventBus buckEventBus, FileSystem fileSystem, BuckGlobals buckGlobals, EventHandler eventHandler,
            GlobberFactory globberFactory) {
        return new SkylarkProjectBuildFileParser(options, buckEventBus, fileSystem, buckGlobals, eventHandler,
                globberFactory);
    }

    @Override
    public BuildFileManifest getBuildFileManifest(Path buildFile)
            throws BuildFileParseException, InterruptedException, IOException {
        ParseResult parseResult = parseBuildFile(buildFile);

        // By contract, BuildFileManifestPojoizer converts any Map to ImmutableMap.
        // ParseResult.getRawRules() returns ImmutableMap<String, Map<String, Object>>, so it is
        // a safe downcast here
        @SuppressWarnings("unchecked")
        ImmutableMap<String, Map<String, Object>> targets = (ImmutableMap<String, Map<String, Object>>) getBuildFileManifestPojoizer()
                .convertToPojo(parseResult.getRawRules());

        return BuildFileManifest.of(targets, ImmutableSortedSet.copyOf(parseResult.getLoadedPaths()),
                parseResult.getReadConfigurationOptions(), Optional.empty(),
                parseResult.getGlobManifestWithResult());
    }

    private static BuildFileManifestPojoizer getBuildFileManifestPojoizer() {
        // Convert Skylark-specific classes to Buck API POJO classes to decouple them from parser
        // implementation. BuildFileManifest should only have POJO classes.
        BuildFileManifestPojoizer pojoizer = BuildFileManifestPojoizer.of();
        pojoizer.addPojoTransformer(
                PojoTransformer.of(com.google.devtools.build.lib.syntax.SelectorList.class, obj -> {
                    com.google.devtools.build.lib.syntax.SelectorList skylarkSelectorList = (com.google.devtools.build.lib.syntax.SelectorList) obj;
                    // recursively convert list elements
                    @SuppressWarnings("unchecked")
                    ImmutableList<Object> elements = (ImmutableList<Object>) pojoizer
                            .convertToPojo(skylarkSelectorList.getElements());
                    return ImmutableListWithSelects.of(elements, skylarkSelectorList.getType());
                }));
        pojoizer.addPojoTransformer(
                PojoTransformer.of(com.google.devtools.build.lib.syntax.SelectorValue.class, obj -> {
                    com.google.devtools.build.lib.syntax.SelectorValue skylarkSelectorValue = (com.google.devtools.build.lib.syntax.SelectorValue) obj;
                    // recursively convert dictionary elements
                    @SuppressWarnings("unchecked")
                    ImmutableMap<String, Object> dictionary = (ImmutableMap<String, Object>) pojoizer
                            .convertToPojo(skylarkSelectorValue.getDictionary());
                    return ImmutableSelectorValue.of(dictionary, skylarkSelectorValue.getNoMatchError());
                }));
        pojoizer.addPojoTransformer(
                PojoTransformer.of(com.google.devtools.build.lib.syntax.SkylarkNestedSet.class, obj -> {
                    com.google.devtools.build.lib.syntax.SkylarkNestedSet skylarkNestedSet = (com.google.devtools.build.lib.syntax.SkylarkNestedSet) obj;
                    // recursively convert set elements
                    return pojoizer.convertToPojo(skylarkNestedSet.toCollection());
                }));
        return pojoizer;
    }

    /**
     * Retrieves build files requested in {@code buildFile}.
     *
     * @param buildFile The build file to parse.
     * @return The {@link ParseResult} with build rules defined in {@code buildFile}.
     */
    private ParseResult parseBuildFile(Path buildFile)
            throws BuildFileParseException, InterruptedException, IOException {
        ImmutableMap<String, Map<String, Object>> rules = ImmutableMap.of();
        ParseBuckFileEvent.Started startEvent = ParseBuckFileEvent.started(buildFile, this.getClass());
        buckEventBus.post(startEvent);
        ParseResult parseResult;
        try {
            parseResult = parseBuildRules(buildFile);
            rules = parseResult.getRawRules();
        } finally {
            buckEventBus.post(ParseBuckFileEvent.finished(startEvent, rules.size(), 0L, Optional.empty()));
        }
        return parseResult;
    }

    private ImplicitlyLoadedExtension loadImplicitExtension(String basePath, Label containingLabel)
            throws IOException, InterruptedException {
        Optional<ImplicitInclude> implicitInclude = packageImplicitIncludeFinder
                .findIncludeForBuildFile(Paths.get(basePath));
        if (!implicitInclude.isPresent()) {
            return ImplicitlyLoadedExtension.empty();
        }

        // Only export requested symbols, and ensure that all requsted symbols are present.
        ExtensionData data = loadExtension(LoadImport.of(containingLabel, implicitInclude.get().getLoadPath()));
        ImmutableMap<String, Object> symbols = data.getExtension().getBindings();
        ImmutableMap<String, String> expectedSymbols = implicitInclude.get().getSymbols();
        Builder<String, Object> loaded = ImmutableMap.builderWithExpectedSize(expectedSymbols.size());
        for (Entry<String, String> kvp : expectedSymbols.entrySet()) {
            Object symbol = symbols.get(kvp.getValue());
            if (symbol == null) {
                throw BuildFileParseException.createForUnknownParseError(
                        String.format("Could not find symbol '%s' in implicitly loaded extension '%s'",
                                kvp.getValue(), implicitInclude.get().getLoadPath().getImportString()));
            }
            loaded.put(kvp.getKey(), symbol);
        }
        return ImplicitlyLoadedExtension.of(data, loaded.build());
    }

    /** @return The parsed build rules defined in {@code buildFile}. */
    private ParseResult parseBuildRules(Path buildFile)
            throws IOException, BuildFileParseException, InterruptedException {
        com.google.devtools.build.lib.vfs.Path buildFilePath = fileSystem.getPath(buildFile.toString());

        String basePath = getBasePath(buildFile);
        Label containingLabel = createContainingLabel(basePath);
        ImplicitlyLoadedExtension implicitLoad = loadImplicitExtension(basePath, containingLabel);

        BuildFileAST buildFileAst = parseBuildFile(buildFilePath, containingLabel);
        CachingGlobber globber = newGlobber(buildFile);
        PackageContext packageContext = createPackageContext(basePath, globber, implicitLoad.getLoadedSymbols());
        ParseContext parseContext = new ParseContext(packageContext);
        try (Mutability mutability = Mutability.create("parsing " + buildFile)) {
            EnvironmentData envData = createBuildFileEvaluationEnvironment(buildFilePath, containingLabel,
                    buildFileAst, mutability, parseContext, implicitLoad.getExtensionData());
            boolean exec = buildFileAst.exec(envData.getEnvironment(), eventHandler);
            if (!exec) {
                throw BuildFileParseException.createForUnknownParseError("Cannot evaluate build file " + buildFile);
            }
            ImmutableMap<String, ImmutableMap<String, Object>> rules = parseContext.getRecordedRules();
            if (LOG.isVerboseEnabled()) {
                LOG.verbose("Got rules: %s", rules.values());
            }
            LOG.verbose("Parsed %d rules from %s", rules.size(), buildFile);
            ImmutableList.Builder<String> loadedPaths = ImmutableList
                    .builderWithExpectedSize(envData.getLoadedPaths().size() + 1);
            loadedPaths.add(buildFilePath.toString());
            loadedPaths.addAll(envData.getLoadedPaths());
            return ParseResult.of(rules, loadedPaths.build(), parseContext.getAccessedConfigurationOptions(),
                    globber.createGlobManifest());
        }
    }

    private BuildFileAST parseBuildFile(com.google.devtools.build.lib.vfs.Path buildFilePath, Label containingLabel)
            throws IOException {
        BuildFileAST buildFileAst = parseSkylarkFile(buildFilePath, containingLabel)
                .validateBuildFile(eventHandler);

        if (buildFileAst.containsErrors()) {
            throw BuildFileParseException.createForUnknownParseError("Cannot parse build file %s", buildFilePath);
        }
        return buildFileAst;
    }

    /** Creates a globber for the package defined by the provided build file path. */
    private CachingGlobber newGlobber(Path buildFile) {
        return CachingGlobber.of(globberFactory.create(fileSystem.getPath(buildFile.getParent().toString())));
    }

    /**
     * @return The environment that can be used for evaluating build files. It includes built-in
     *     functions like {@code glob} and native rules like {@code java_library}.
     */
    private EnvironmentData createBuildFileEvaluationEnvironment(
            com.google.devtools.build.lib.vfs.Path buildFilePath, Label containingLabel, BuildFileAST buildFileAst,
            Mutability mutability, ParseContext parseContext, @Nullable ExtensionData implicitLoadExtensionData)
            throws IOException, InterruptedException, BuildFileParseException {
        ImmutableList<ExtensionData> dependencies = loadExtensions(containingLabel, buildFileAst.getImports());
        ImmutableMap<String, Environment.Extension> importMap = toImportMap(dependencies,
                implicitLoadExtensionData);
        Environment env = Environment.builder(mutability).setImportedExtensions(importMap)
                .setGlobals(buckGlobals.getBuckBuildFileContextGlobals())
                .setSemantics(StarlarkSemantics.DEFAULT_SEMANTICS).setEventHandler(eventHandler).build();
        SkylarkUtils.setPhase(env, Phase.LOADING);

        parseContext.setup(env);

        return EnvironmentData.of(env, toLoadedPaths(buildFilePath, dependencies, implicitLoadExtensionData));
    }

    @Nonnull
    private PackageContext createPackageContext(String basePath, Globber globber,
            ImmutableMap<String, Object> implicitlyLoadedSymbols) {
        return PackageContext.of(globber, options.getRawConfig(),
                PackageIdentifier.create(RepositoryName.createFromValidStrippedName(options.getCellName()),
                        PathFragment.create(basePath)),
                eventHandler, implicitlyLoadedSymbols);
    }

    private Label createContainingLabel(String basePath) {
        return Label.createUnvalidated(
                PackageIdentifier.create(RepositoryName.createFromValidStrippedName(options.getCellName()),
                        PathFragment.create(basePath)),
                "BUCK");
    }

    /**
     * @param containingPath the path of the build or extension file that has provided dependencies.
     * @param dependencies the list of extension dependencies that {@code containingPath} has.
     * @return transitive closure of all paths loaded during parsing of {@code containingPath}
     *     including {@code containingPath} itself as the first element.
     */
    private ImmutableList<String> toLoadedPaths(com.google.devtools.build.lib.vfs.Path containingPath,
            ImmutableList<ExtensionData> dependencies, @Nullable ExtensionData implicitLoadExtensionData) {
        // expected size is used to reduce the number of unnecessary resize invocations
        int expectedSize = 1;
        if (implicitLoadExtensionData != null) {
            expectedSize += implicitLoadExtensionData.getLoadTransitiveClosure().size();
        }
        for (int i = 0; i < dependencies.size(); ++i) {
            expectedSize += dependencies.get(i).getLoadTransitiveClosure().size();
        }
        ImmutableList.Builder<String> loadedPathsBuilder = ImmutableList.builderWithExpectedSize(expectedSize);
        // for loop is used instead of foreach to avoid iterator overhead, since it's a hot spot
        loadedPathsBuilder.add(containingPath.toString());
        for (int i = 0; i < dependencies.size(); ++i) {
            loadedPathsBuilder.addAll(dependencies.get(i).getLoadTransitiveClosure());
        }
        if (implicitLoadExtensionData != null) {
            loadedPathsBuilder.addAll(implicitLoadExtensionData.getLoadTransitiveClosure());
        }
        return loadedPathsBuilder.build();
    }

    /**
     * Reads file and returns abstract syntax tree for that file.
     *
     * @param path file path to read the data from.
     * @return abstract syntax tree; does not handle any errors.
     */
    @VisibleForTesting
    protected BuildFileAST readSkylarkAST(com.google.devtools.build.lib.vfs.Path path) throws IOException {
        return BuildFileAST.parseSkylarkFile(ParserInputSource.create(
                FileSystemUtils.readContent(path, StandardCharsets.UTF_8), path.asFragment()), eventHandler);
    }

    private BuildFileAST parseSkylarkFile(com.google.devtools.build.lib.vfs.Path path, Label containingLabel)
            throws BuildFileParseException, IOException {
        BuildFileAST result = astCache.getIfPresent(path);
        if (result == null) {
            try {
                result = readSkylarkAST(path);
            } catch (FileNotFoundException e) {
                throw BuildFileParseException.createForUnknownParseError(
                        "%s cannot be loaded because it does not exist. It was referenced from %s", path,
                        containingLabel);
            }
            if (result.containsErrors()) {
                throw BuildFileParseException.createForUnknownParseError(
                        "Cannot parse %s.  It was referenced form %s", path, containingLabel);
            }
            astCache.put(path, result);
        }
        return result;
    }

    /**
     * Creates an {@code IncludesData} object from a {@code path}.
     *
     * @param loadImport an import label representing an extension to load.
     */
    private IncludesData loadInclude(LoadImport loadImport)
            throws IOException, BuildFileParseException, InterruptedException {
        Label label = loadImport.getLabel();
        com.google.devtools.build.lib.vfs.Path filePath = getImportPath(label, loadImport.getImport());

        BuildFileAST fileAst = parseSkylarkFile(filePath, loadImport.getContainingLabel());

        ImmutableList<IncludesData> dependencies = fileAst.getImports().isEmpty() ? ImmutableList.of()
                : loadIncludes(label, fileAst.getImports());

        return IncludesData.of(filePath, dependencies, toIncludedPaths(filePath.toString(), dependencies, null));
    }

    /** Collects all the included files identified by corresponding {@link SkylarkImport}s. */
    private ImmutableList<IncludesData> loadIncludes(Label containingLabel,
            ImmutableList<SkylarkImport> skylarkImports)
            throws BuildFileParseException, IOException, InterruptedException {
        Set<SkylarkImport> processed = new HashSet<>(skylarkImports.size());
        ImmutableList.Builder<IncludesData> includes = ImmutableList.builderWithExpectedSize(skylarkImports.size());
        // foreach is not used to avoid iterator overhead
        for (int i = 0; i < skylarkImports.size(); ++i) {
            SkylarkImport skylarkImport = skylarkImports.get(i);
            // sometimes users include the same extension multiple times...
            if (!processed.add(skylarkImport))
                continue;
            LoadImport loadImport = LoadImport.of(containingLabel, skylarkImport);
            try {
                includes.add(includesDataCache.getUnchecked(loadImport));
            } catch (UncheckedExecutionException e) {
                propagateRootCause(e);
            }
        }
        return includes.build();
    }

    private ImmutableSet<String> toIncludedPaths(String containingPath, ImmutableList<IncludesData> dependencies,
            @Nullable ExtensionData implicitLoadExtensionData) {
        ImmutableSet.Builder<String> includedPathsBuilder = ImmutableSet.builder();
        includedPathsBuilder.add(containingPath);
        // for loop is used instead of foreach to avoid iterator overhead, since it's a hot spot
        for (int i = 0; i < dependencies.size(); ++i) {
            includedPathsBuilder.addAll(dependencies.get(i).getLoadTransitiveClosure());
        }
        if (implicitLoadExtensionData != null) {
            includedPathsBuilder.addAll(implicitLoadExtensionData.getLoadTransitiveClosure());
        }
        return includedPathsBuilder.build();
    }

    /** Loads all extensions identified by corresponding {@link SkylarkImport}s. */
    private ImmutableList<ExtensionData> loadExtensions(Label containingLabel,
            ImmutableList<SkylarkImport> skylarkImports)
            throws BuildFileParseException, IOException, InterruptedException {
        Set<SkylarkImport> processed = new HashSet<>(skylarkImports.size());
        ImmutableList.Builder<ExtensionData> extensions = ImmutableList
                .builderWithExpectedSize(skylarkImports.size());
        // foreach is not used to avoid iterator overhead
        for (int i = 0; i < skylarkImports.size(); ++i) {
            SkylarkImport skylarkImport = skylarkImports.get(i);
            // sometimes users include the same extension multiple times...
            if (!processed.add(skylarkImport))
                continue;
            LoadImport loadImport = LoadImport.of(containingLabel, skylarkImport);
            try {
                extensions.add(extensionDataCache.getUnchecked(loadImport));
            } catch (UncheckedExecutionException e) {
                propagateRootCause(e);
            }
        }
        return extensions.build();
    }

    /**
     * Propagates underlying parse exception from {@link UncheckedExecutionException}.
     *
     * <p>This is an unfortunate consequence of having to use {@link
     * LoadingCache#getUnchecked(Object)} in when using stream transformations :(
     *
     * <p>TODO(ttsugrii): the logic of extracting root causes to make them user-friendly should be
     * happening somewhere in {@link com.facebook.buck.cli.Main#main(String[])}, since this behavior
     * is not unique to parsing.
     */
    private void propagateRootCause(UncheckedExecutionException e) throws IOException, InterruptedException {
        Throwable rootCause = Throwables.getRootCause(e);
        if (rootCause instanceof BuildFileParseException) {
            throw (BuildFileParseException) rootCause;
        }
        if (rootCause instanceof IOException) {
            throw (IOException) rootCause;
        }
        if (rootCause instanceof InterruptedException) {
            throw (InterruptedException) rootCause;
        }
        throw e;
    }

    /**
     * @return The map from skylark import string like {@code //pkg:build_rules.bzl} to an {@link
     *     Environment.Extension} for provided {@code dependencies}.
     */
    private ImmutableMap<String, Environment.Extension> toImportMap(ImmutableList<ExtensionData> dependencies,
            @Nullable ExtensionData implicitLoadExtensionData) {
        ImmutableMap.Builder<String, Environment.Extension> builder = ImmutableMap
                .builderWithExpectedSize(dependencies.size() + (implicitLoadExtensionData == null ? 0 : 1));
        // foreach is not used to avoid iterator overhead
        for (int i = 0; i < dependencies.size(); ++i) {
            ExtensionData extensionData = dependencies.get(i);
            builder.put(extensionData.getImportString(), extensionData.getExtension());
        }
        if (implicitLoadExtensionData != null) {
            builder.put(implicitLoadExtensionData.getImportString(), implicitLoadExtensionData.getExtension());
        }
        return builder.build();
    }

    /**
     * Creates an extension from a {@code path}.
     *
     * @param loadImport an import label representing an extension to load.
     */
    private ExtensionData loadExtension(LoadImport loadImport)
            throws IOException, BuildFileParseException, InterruptedException {
        Label label = loadImport.getLabel();
        com.google.devtools.build.lib.vfs.Path extensionPath = getImportPath(label, loadImport.getImport());
        ImmutableList<ExtensionData> dependencies = ImmutableList.of();
        Extension extension;
        try (Mutability mutability = Mutability.create("importing extension")) {
            BuildFileAST extensionAst = parseSkylarkFile(extensionPath, loadImport.getContainingLabel());
            Environment.Builder envBuilder = Environment.builder(mutability).setEventHandler(eventHandler)
                    .setGlobals(buckGlobals.getBuckLoadContextGlobals());
            if (!extensionAst.getImports().isEmpty()) {
                dependencies = loadExtensions(label, extensionAst.getImports());
                envBuilder.setImportedExtensions(toImportMap(dependencies, null));
            }
            Environment extensionEnv = envBuilder.setSemantics(StarlarkSemantics.DEFAULT_SEMANTICS).build();
            boolean success = extensionAst.exec(extensionEnv, eventHandler);
            if (!success) {
                throw BuildFileParseException.createForUnknownParseError(
                        "Cannot evaluate extension file " + loadImport.getImport().getImportString());
            }
            extension = new Extension(extensionEnv);
        }

        return ExtensionData.of(extension, extensionPath, dependencies, loadImport.getImport().getImportString(),
                toLoadedPaths(extensionPath, dependencies, null));
    }

    /**
     * @return The path to a Skylark extension. For example, for {@code load("//pkg:foo.bzl", "foo")}
     *     import it would return {@code /path/to/repo/pkg/foo.bzl} and for {@code
     *     load("@repo//pkg:foo.bzl", "foo")} it would return {@code /repo/pkg/foo.bzl} assuming that
     *     {@code repo} is located at {@code /repo}.
     */
    private com.google.devtools.build.lib.vfs.Path getImportPath(Label containingLabel, SkylarkImport skylarkImport)
            throws BuildFileParseException {
        if (isRelativeLoad(skylarkImport) && skylarkImport.getImportString().contains("/")) {
            throw BuildFileParseException
                    .createForUnknownParseError("Relative loads work only for files in the same directory but "
                            + skylarkImport.getImportString()
                            + " is trying to load a file from a nested directory. "
                            + "Please use absolute label instead ([cell]//pkg[/pkg]:target).");
        }
        PathFragment relativeExtensionPath = containingLabel.toPathFragment();
        RepositoryName repository = containingLabel.getPackageIdentifier().getRepository();
        if (repository.isMain()) {
            return fileSystem
                    .getPath(options.getProjectRoot().resolve(relativeExtensionPath.toString()).toString());
        }
        // Skylark repositories have an "@" prefix, but Buck roots do not, so ignore it
        String repositoryName = repository.getName().substring(1);
        @Nullable
        Path repositoryPath = options.getCellRoots().get(repositoryName);
        if (repositoryPath == null) {
            throw BuildFileParseException.createForUnknownParseError(
                    skylarkImport.getImportString() + " references an unknown repository " + repositoryName);
        }
        return fileSystem.getPath(repositoryPath.resolve(relativeExtensionPath.toString()).toString());
    }

    private boolean isRelativeLoad(SkylarkImport skylarkImport) {
        return skylarkImport.getImportString().startsWith(":");
    }

    /**
     * @return The path path of the provided {@code buildFile}. For example, for {@code
     *     /Users/foo/repo/src/bar/BUCK}, where {@code /Users/foo/repo} is the path to the repo, it
     *     would return {@code src/bar}.
     */
    private String getBasePath(Path buildFile) {
        return Optional.ofNullable(options.getProjectRoot().relativize(buildFile).getParent())
                .map(MorePaths::pathWithUnixSeparators).orElse("");
    }

    @Override
    public void reportProfile() {
        // this method is a noop since Skylark profiling is completely orthogonal to parsing and is
        // controlled by com.google.devtools.build.lib.profiler.Profiler
    }

    @Override
    public ImmutableSortedSet<String> getIncludedFiles(Path buildFile)
            throws BuildFileParseException, InterruptedException, IOException {
        com.google.devtools.build.lib.vfs.Path buildFilePath = fileSystem.getPath(buildFile.toString());

        String basePath = getBasePath(buildFile);
        Label containingLabel = createContainingLabel(basePath);
        ImplicitlyLoadedExtension implicitLoad = loadImplicitExtension(basePath, containingLabel);
        BuildFileAST buildFileAst = parseBuildFile(buildFilePath, containingLabel);
        ImmutableList<IncludesData> dependencies = loadIncludes(containingLabel, buildFileAst.getImports());

        // it might be potentially faster to keep sorted sets for each dependency separately and just
        // merge sorted lists as we aggregate transitive close up
        // But Guava does not seem to have a built-in way of merging sorted lists/sets
        return ImmutableSortedSet
                .copyOf(toIncludedPaths(buildFile.toString(), dependencies, implicitLoad.getExtensionData()));
    }

    @Override
    public boolean globResultsMatchCurrentState(Path buildFile,
            ImmutableList<GlobSpecWithResult> existingGlobsWithResults) throws IOException, InterruptedException {
        CachingGlobber globber = newGlobber(buildFile);
        for (GlobSpecWithResult globSpecWithResult : existingGlobsWithResults) {
            final GlobSpec globSpec = globSpecWithResult.getGlobSpec();
            Set<String> globResult = globber.run(globSpec.getInclude(), globSpec.getExclude(),
                    globSpec.getExcludeDirectories());
            if (!globSpecWithResult.getFilePaths().equals(globResult)) {
                return false;
            }
        }

        return true;
    }

    @Override
    public void close() throws BuildFileParseException {
        // nothing to do
    }

    /**
     * A value object for information about load function import, since {@link SkylarkImport} does not
     * provide enough context. For instance, the same {@link SkylarkImport} can represent different
     * logical imports depending on which repository it is resolved in.
     */
    @Value.Immutable(builder = false)
    @BuckStyleImmutable
    abstract static class AbstractLoadImport {
        /** Returns a label of the file containing this import. */
        @Value.Parameter
        abstract Label getContainingLabel();

        /** Returns a Skylark import. */
        @Value.Parameter
        abstract SkylarkImport getImport();

        /** Returns a label of current import file. */
        Label getLabel() {
            return getImport().getLabel(getContainingLabel());
        }
    }

    /**
     * A value object for information about implicit loads. This allows us to both validate implicit
     * import information, and return some additional information needed to setup build file
     * environments in one swoop.
     */
    @Value.Immutable(builder = false, copy = false)
    @BuckStyleImmutable
    abstract static class AbstractImplicitlyLoadedExtension {
        @Value.Parameter
        abstract @Nullable ExtensionData getExtensionData();

        @Value.Parameter
        abstract ImmutableMap<String, Object> getLoadedSymbols();

        @Value.Lazy
        static ImplicitlyLoadedExtension empty() {
            return ImplicitlyLoadedExtension.of(null, ImmutableMap.of());
        }
    }
}