// Copyright 2017 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.skylark.skylint; import com.google.common.collect.ImmutableList; import com.google.devtools.build.lib.syntax.BuildFileAST; import com.google.devtools.build.lib.syntax.LoadStatement; import com.google.devtools.build.lib.syntax.Statement; import com.google.devtools.skylark.skylint.Linter.FileFacade; import java.io.IOException; import java.nio.file.Path; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; /** * Helps collect information about direct and transitive dependencies of a Skylark file. * * @param the type of information associated with a file */ public class DependencyAnalyzer { private final FileFacade fileFacade; private final Map pathToInfo = new LinkedHashMap<>(); private Map cachedWorkspaceRoot = new LinkedHashMap<>(); private Map cachedPackageRoot = new LinkedHashMap<>(); private DependencyCollector collector; private Set visited = new LinkedHashSet<>(); private static final ImmutableList BUILD_FILES = ImmutableList.of("BUILD", "BUILD.bazel"); private static final ImmutableList WORKSPACE_FILE = ImmutableList.of("WORKSPACE"); /** * Creates an instance of DependencyAnalyzer that can be reused for multiple Skylark files. * * @param fileFacade interface to access file contents * @param collector extracts the desired information from a Skylark file */ public DependencyAnalyzer(FileFacade fileFacade, DependencyCollector collector) { this.fileFacade = fileFacade; this.collector = collector; } /** * Collects information about the given file and its direct and transitive dependencies. * *

The information is cached between calls to this method, so it won't reanalyze the same file * twice. This applies even if a file is loaded via different path labels that correspond to the * same canonical path. * * @param path the path of the file to be analyzed * @return the information about that file, or null if it can't be read */ @Nullable public T collectTransitiveInfo(Path path) { path = path.toAbsolutePath(); if (visited.contains(path)) { return pathToInfo.get(path); } visited.add(path); BuildFileAST ast; try { ast = fileFacade.readAst(path); } catch (IOException e) { return null; } T info = collector.initInfo(path); for (Statement stmt : ast.getStatements()) { if (stmt instanceof LoadStatement) { String label = ((LoadStatement) stmt).getImport().getValue(); Path dep = labelToPath(label, path); if (dep == null) { continue; } T depInfo = collectTransitiveInfo(dep); if (depInfo == null) { continue; // may happen if there's an illegal dependency cycle } info = collector.loadDependency(info, (LoadStatement) stmt, dep, depInfo); } } info = collector.collectInfo(path, ast, info); pathToInfo.put(path, info); return info; } @Nullable private Path findAncestorDirectoryContainingAnyOf(Path path, Iterable fileNames) { Path dir = path.toAbsolutePath(); while ((dir = dir.getParent()) != null) { for (String fileName : fileNames) { if (fileFacade.fileExists(dir.resolve(fileName))) { return dir; } } } return null; } /** * Resolves the label of a load statement to a path. * * @param label the import of a load statement * @param currentPath the path of the file containing the load statement * @return the path corresponding to the label or null if it can't be resolved */ @Nullable private Path labelToPath(String label, Path currentPath) { if (label.startsWith("@")) { // TODO(skylark-team): analyze such dependencies as well return null; } else if (label.startsWith("//")) { Path workspaceRoot = getWorkspaceRoot(currentPath); if (workspaceRoot == null) { return null; } label = label.substring(label.startsWith("//:") ? 3 : 2); return workspaceRoot.resolve(label.replace(':', '/')); } else if (label.startsWith(":")) { Path packageRoot = getPackageRoot(currentPath); if (packageRoot == null) { return null; } return packageRoot.resolve(label.substring(1)); } else { // otherwise just treat it as a though it started with "//" Path workspaceRoot = getWorkspaceRoot(currentPath); if (workspaceRoot == null) { return null; } return workspaceRoot.resolve(label.replace(':', '/')); } } @Nullable private Path getPackageRoot(Path path) { if (!cachedPackageRoot.containsKey(path)) { cachedPackageRoot.put(path, findAncestorDirectoryContainingAnyOf(path, BUILD_FILES)); } return cachedPackageRoot.get(path); } @Nullable private Path getWorkspaceRoot(Path path) { if (!cachedWorkspaceRoot.containsKey(path)) { cachedWorkspaceRoot.put(path, findAncestorDirectoryContainingAnyOf(path, WORKSPACE_FILE)); } return cachedWorkspaceRoot.get(path); } /** * Encapsulates how to produce information about a Skylark file, given its contents and the * information about its transitive dependencies. * *

Each of the methods returns new or updated instances of T associated with the file. * *

When analyzing a file, the methods will be invoked in the following order: * *

    *
  1. {@link DependencyCollector#initInfo} to return an initial info object *
  2. {@link DependencyCollector#loadDependency} is iteratively called for each direct * dependency to get an updated info object that accounts for the info from this dependency, * until all direct dependencies have been processed *
  3. {@link DependencyCollector#collectInfo} is called to get a final updated info object that * accounts for the content of the current file. *
* * @param the type of information being collected */ public interface DependencyCollector { /** * Used to initialize the dependency information when starting analysis of a file. * * @param path the path of the current file * @return the initial information about the current file */ T initInfo(Path path); /** * Incorporates the information about a dependency in the information about the file. * *

This method is called for every load() statement in the current file. * * @param currentFileInfo info about the current file so far, may be modified * @param stmt the load statement being processed * @param loadedPath the path of the dependency that is load()ed * @param loadedFileInfo info about the dependency file that is load()ed, must not be modified * @return the updated information about the current file */ T loadDependency(T currentFileInfo, LoadStatement stmt, Path loadedPath, T loadedFileInfo); /** * Collect information about the current file after the load statements have been processed. * * @param path the path of the current file * @param ast the ast of the current file * @param info the information about the current file so far, may be modified * @return the updated information about the current file */ T collectInfo(Path path, BuildFileAST ast, T info); } }