|  | // 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.includescanning; | 
|  |  | 
|  | import static java.nio.charset.StandardCharsets.ISO_8859_1; | 
|  |  | 
|  | import com.google.common.annotations.VisibleForTesting; | 
|  | import com.google.common.base.Preconditions; | 
|  | 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.Sets; | 
|  | import com.google.common.io.CharStreams; | 
|  | import com.google.devtools.build.lib.actions.ActionExecutionContext; | 
|  | import com.google.devtools.build.lib.actions.ActionExecutionMetadata; | 
|  | import com.google.devtools.build.lib.actions.Artifact; | 
|  | import com.google.devtools.build.lib.actions.ArtifactFactory; | 
|  | import com.google.devtools.build.lib.actions.ArtifactRoot; | 
|  | import com.google.devtools.build.lib.actions.ExecException; | 
|  | import com.google.devtools.build.lib.cmdline.PackageIdentifier; | 
|  | import com.google.devtools.build.lib.events.Event; | 
|  | import com.google.devtools.build.lib.includescanning.IncludeParser.Inclusion.Kind; | 
|  | import com.google.devtools.build.lib.packages.NoSuchPackageException; | 
|  | import com.google.devtools.build.lib.profiler.Profiler; | 
|  | import com.google.devtools.build.lib.profiler.ProfilerTask; | 
|  | import com.google.devtools.build.lib.profiler.SilentCloseable; | 
|  | import com.google.devtools.build.lib.skyframe.ContainingPackageLookupValue; | 
|  | import com.google.devtools.build.lib.skyframe.GlobDescriptor; | 
|  | import com.google.devtools.build.lib.skyframe.GlobValue; | 
|  | import com.google.devtools.build.lib.skyframe.GlobValue.InvalidGlobPatternException; | 
|  | import com.google.devtools.build.lib.skyframe.PerBuildSyscallCache; | 
|  | import com.google.devtools.build.lib.vfs.FileSystemUtils; | 
|  | import com.google.devtools.build.lib.vfs.Path; | 
|  | import com.google.devtools.build.lib.vfs.PathFragment; | 
|  | import com.google.devtools.build.lib.vfs.Root; | 
|  | import com.google.devtools.build.lib.vfs.UnixGlob; | 
|  | import com.google.devtools.build.lib.vfs.UnixGlob.FilesystemCalls; | 
|  | import com.google.devtools.build.skyframe.SkyFunction.Environment; | 
|  | import com.google.devtools.build.skyframe.SkyKey; | 
|  | import com.google.devtools.build.skyframe.SkyValue; | 
|  | import com.google.devtools.build.skyframe.ValueOrException; | 
|  | import java.io.IOException; | 
|  | import java.io.InputStream; | 
|  | import java.io.InputStreamReader; | 
|  | import java.util.ArrayList; | 
|  | import java.util.Collection; | 
|  | import java.util.List; | 
|  | import java.util.Map; | 
|  | import java.util.Set; | 
|  | import java.util.concurrent.atomic.AtomicReference; | 
|  | import java.util.logging.Level; | 
|  | import java.util.logging.Logger; | 
|  | import java.util.regex.Matcher; | 
|  | import java.util.regex.Pattern; | 
|  | import java.util.regex.PatternSyntaxException; | 
|  | import javax.annotation.Nullable; | 
|  |  | 
|  | /** | 
|  | * Scans a source file and extracts the literal inclusions it specifies. Does not store results -- | 
|  | * repeated requests to the same file will result in repeated scans. Clients should implement a | 
|  | * caching layer in order to avoid unnecessary disk access when requesting an already scanned file. | 
|  | */ | 
|  | @VisibleForTesting | 
|  | class IncludeParser { | 
|  |  | 
|  | /** | 
|  | * File types supported by the grep-includes binary. {@link #fileType} must be kept is sync with | 
|  | * //tools/cpp:grep-includes. | 
|  | */ | 
|  | public enum GrepIncludesFileType { | 
|  | CPP("c++"), | 
|  | SWIG("swig"); | 
|  |  | 
|  | private final String fileType; | 
|  |  | 
|  | GrepIncludesFileType(String fileType) { | 
|  | this.fileType = fileType; | 
|  | } | 
|  |  | 
|  | public String getFileType() { | 
|  | return fileType; | 
|  | } | 
|  | } | 
|  |  | 
|  | private static final Logger logger = Logger.getLogger(IncludeParser.class.getName()); | 
|  | private static final boolean LOG_FINE = logger.isLoggable(Level.FINE); | 
|  |  | 
|  | /** | 
|  | * Immutable object representation of the four columns making up a single Rule | 
|  | * in a Hints set. See {@link Hints} for more details. | 
|  | */ | 
|  | private static class Rule { | 
|  | private enum Type { PATH, FILE, INCLUDE_QUOTE, INCLUDE_ANGLE } | 
|  | final Type type; | 
|  | final Pattern pattern; | 
|  | final String findRoot; | 
|  | final String findFilter; | 
|  |  | 
|  | private Rule(String type, String pattern, String findRoot, String findFilter) { | 
|  | this.type = Type.valueOf(type.trim().toUpperCase()); | 
|  | this.pattern = Pattern.compile("^" + pattern + "$"); | 
|  | this.findRoot = findRoot.replace('\\', '$'); | 
|  | this.findFilter = findFilter; | 
|  | } | 
|  |  | 
|  | Rule(String type, String pattern, String findRoot) { | 
|  | this(type, pattern, findRoot, null); | 
|  | Preconditions.checkArgument((this.type == Type.INCLUDE_QUOTE) | 
|  | || (this.type == Type.INCLUDE_ANGLE), this); | 
|  | } | 
|  |  | 
|  | @Override public String toString() { | 
|  | return "" + type + " " + pattern + " " + findRoot + " " + findFilter; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** {@link SkyValue} encapsulating the source-state-dependent part of {@link Hints}. */ | 
|  | public static class HintsRules implements SkyValue { | 
|  | private final ImmutableList<Rule> rules; | 
|  |  | 
|  | private HintsRules(ImmutableList<Rule> rules) { | 
|  | this.rules = rules; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * This class is a representation of the INCLUDE_HINTS file. The hints file contains regexp-based | 
|  | * rules to help this simple include scanner cope with computed includes, which would otherwise | 
|  | * require a full preprocessor with symbol support. Instead of actually processing symbols to | 
|  | * evaluate the computed includes, we instead apply rules to gather inclusions for matching paths. | 
|  | * | 
|  | * <p>The hints file is read, line by line, into a list of rules each of which encapsulates a line | 
|  | * of four columns. Each non-blank, non-comment line has the format: | 
|  | * | 
|  | * <pre> | 
|  | *   "file"|"path"  match-pattern  find-root  find-filter | 
|  | * </pre> | 
|  | * | 
|  | * <p>The first column specifies whether the line is a rule based on matching source | 
|  | * <em>files</em> (passed directly to the compiler as inputs, or transitively #included by other | 
|  | * inputs) or include <em>paths</em> (passed to the compiler as -I, -iquote, or -isystem flags). | 
|  | * | 
|  | * <p>The second column is a regexp for files or paths. Whenever a compiler argument of the | 
|  | * specified type matches that regexp, the rule is taken. (All matching rules for every path and | 
|  | * file on a compiler command line are followed, and the results are combined.) | 
|  | * | 
|  | * <p>The third column is a point in the local filesystem from which to extract a recursive | 
|  | * listing. (This follows symlinks) Backrefs may be used to refer to the regexp or its capturing | 
|  | * groups. (This is mostly necessary because --package_path can cause input paths to carry | 
|  | * arbitrary prefixes.) | 
|  | * | 
|  | * <p>The fourth column is a regexp applied to each file found by the recursive listing. All | 
|  | * matching files are treated as dependencies. | 
|  | */ | 
|  | public static class Hints { | 
|  | private static final Pattern WS_PAT = Pattern.compile("\\s+"); | 
|  | @VisibleForTesting | 
|  | static final String ALLOWED_PREFIX = "third_party/"; | 
|  | // Match regular expressions that can only match paths under ALLOWED_PREFIX . | 
|  | private static final Pattern ALLOWED_PATTERN = Pattern.compile("^\\(*" + ALLOWED_PREFIX + ".*"); | 
|  |  | 
|  | private static final int HINTS_CACHE_CONCURRENCY = 100; | 
|  |  | 
|  | private final ImmutableList<Rule> rules; | 
|  | private final ArtifactFactory artifactFactory; | 
|  |  | 
|  | private final AtomicReference<FilesystemCalls> syscallCache = new AtomicReference<>(); | 
|  |  | 
|  | private final LoadingCache<Artifact, Collection<Artifact>> fileLevelHintsCache = | 
|  | CacheBuilder.newBuilder().concurrencyLevel(HINTS_CACHE_CONCURRENCY).build( | 
|  | new CacheLoader<Artifact, Collection<Artifact>>() { | 
|  | @Override | 
|  | public Collection<Artifact> load(Artifact path) { | 
|  | return getHintedInclusionsLegacy(Rule.Type.FILE, | 
|  | path.getExecPath(), path.getRoot()); | 
|  | } | 
|  | }); | 
|  |  | 
|  | /** | 
|  | * Constructs a hint set for a given INCLUDE_HINTS file to read. | 
|  | * | 
|  | * @param hintsRules the {@link HintsRules} parsed from INCLUDE_HINTS | 
|  | */ | 
|  | public Hints(HintsRules hintsRules, ArtifactFactory artifactFactory) { | 
|  | this.artifactFactory = artifactFactory; | 
|  | this.rules = hintsRules.rules; | 
|  | clearCachedLegacyHints(); | 
|  | } | 
|  |  | 
|  | static HintsRules getRules(Path hintsFile) throws IOException { | 
|  | ImmutableList.Builder<Rule> rules = ImmutableList.builder(); | 
|  | try (InputStream is = hintsFile.getInputStream()) { | 
|  | for (String line : CharStreams.readLines(new InputStreamReader(is, "UTF-8"))) { | 
|  | line = line.trim(); | 
|  | if (line.length() == 0 || line.startsWith("#")) { | 
|  | continue; | 
|  | } | 
|  | String[] tokens = WS_PAT.split(line); | 
|  | try { | 
|  | if (tokens.length == 3) { | 
|  | rules.add(new Rule(tokens[0], tokens[1], tokens[2])); | 
|  | } else if (tokens.length == 4) { | 
|  | if (!ALLOWED_PATTERN.matcher(tokens[1]).matches()) { | 
|  | throw new IOException("Illegal hint regex on: " + line + "\n" | 
|  | + tokens[1] + " does not match only paths in " + ALLOWED_PREFIX); | 
|  | } | 
|  | rules.add(new Rule(tokens[0], tokens[1], tokens[2], tokens[3])); | 
|  | } else { | 
|  | throw new IOException("Malformed hint line: " + line); | 
|  | } | 
|  | } catch (PatternSyntaxException e) { | 
|  | throw new IOException("Malformed hint regex on: " + line + "\n  " + e.getMessage()); | 
|  | } catch (IllegalArgumentException e) { | 
|  | throw new IOException("Invalid type on: " + line + "\n  " + e.getMessage()); | 
|  | } | 
|  | } | 
|  | } | 
|  | return new HintsRules(rules.build()); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Clears legacy inclusions cache to maintain inter-build correctness, since filesystem changes | 
|  | * are not tracked by cache. | 
|  | */ | 
|  | void clearCachedLegacyHints() { | 
|  | fileLevelHintsCache.invalidateAll(); | 
|  | syscallCache.set( | 
|  | PerBuildSyscallCache.newBuilder().setConcurrencyLevel(HINTS_CACHE_CONCURRENCY).build()); | 
|  | } | 
|  |  | 
|  | /** Returns the "file" type hinted inclusions for a given path, caching results by path. */ | 
|  | Collection<Artifact> getFileLevelHintedInclusionsLegacy(Artifact path) { | 
|  | if (!path.getExecPathString().startsWith(ALLOWED_PREFIX)) { | 
|  | return ImmutableList.of(); | 
|  | } | 
|  | return fileLevelHintsCache.getUnchecked(path); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns the "path" type hinted inclusions for the given paths. Callers are responsible for | 
|  | * caching. | 
|  | */ | 
|  | Collection<Artifact> getPathLevelHintedInclusions( | 
|  | ImmutableList<PathFragment> paths, Environment env) throws InterruptedException { | 
|  | return getHintedInclusionsWithSkyframe(Rule.Type.PATH, paths, env); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Performs the work of matching the given paths against the hints and returns the matching | 
|  | * files. This is semantically different from {@link #getHintedInclusionsLegacy} in that it will | 
|  | * not cross package boundaries. | 
|  | */ | 
|  | private Collection<Artifact> getHintedInclusionsWithSkyframe( | 
|  | Rule.Type type, ImmutableList<PathFragment> paths, Environment env) | 
|  | throws InterruptedException { | 
|  | ImmutableList<String> pathStrings = | 
|  | paths.stream() | 
|  | .map(PathFragment::getPathString) | 
|  | .filter((p) -> p.startsWith(ALLOWED_PREFIX)) | 
|  | .collect(ImmutableList.toImmutableList()); | 
|  | if (pathStrings.isEmpty()) { | 
|  | return ImmutableList.of(); | 
|  | } | 
|  | // Delay creation until we know we need one. Use a TreeSet to make sure that the results are | 
|  | // sorted with a stable order and unique. | 
|  | Set<Artifact> hints = null; | 
|  | List<ContainingPackageLookupValue.Key> rulePaths = new ArrayList<>(rules.size()); | 
|  | List<String> findFilters = new ArrayList<>(rules.size()); | 
|  | for (Rule rule : rules) { | 
|  | if (type != rule.type) { | 
|  | continue; | 
|  | } | 
|  | String firstMatchPathString = null; | 
|  | Matcher m = null; | 
|  | for (String pathString : pathStrings) { | 
|  | m = rule.pattern.matcher(pathString); | 
|  | if (m.matches()) { | 
|  | firstMatchPathString = pathString; | 
|  | break; | 
|  | } | 
|  | } | 
|  | if (firstMatchPathString == null) { | 
|  | continue; | 
|  | } | 
|  | if (hints == null) { | 
|  | hints = Sets.newTreeSet(Artifact.EXEC_PATH_COMPARATOR); | 
|  | } | 
|  | PathFragment relativePath = PathFragment.create(m.replaceFirst(rule.findRoot)); | 
|  | if (LOG_FINE) { | 
|  | logger.fine( | 
|  | "hint for " + rule.type + " " + firstMatchPathString + " root: " + relativePath); | 
|  | } | 
|  | if (!relativePath.getPathString().startsWith(ALLOWED_PREFIX)) { | 
|  | logger.warning( | 
|  | "Path " | 
|  | + relativePath.getPathString() | 
|  | + " to search after substitution does not start with " | 
|  | + ALLOWED_PREFIX); | 
|  | continue; | 
|  | } | 
|  | rulePaths.add( | 
|  | ContainingPackageLookupValue.key(PackageIdentifier.createInMainRepo(relativePath))); | 
|  | findFilters.add(rule.findFilter); | 
|  | } | 
|  | Map<SkyKey, ValueOrException<NoSuchPackageException>> containingPackageLookupValues = | 
|  | env.getValuesOrThrow(rulePaths, NoSuchPackageException.class); | 
|  | if (env.valuesMissing()) { | 
|  | return null; | 
|  | } | 
|  | List<GlobDescriptor> globKeys = new ArrayList<>(rulePaths.size()); | 
|  | for (int i = 0; i < rulePaths.size(); i++) { | 
|  | ContainingPackageLookupValue containingPackageLookupValue; | 
|  | ContainingPackageLookupValue.Key relativePathKey = rulePaths.get(i); | 
|  | PathFragment relativePath = relativePathKey.argument().getPackageFragment(); | 
|  | try { | 
|  | containingPackageLookupValue = | 
|  | (ContainingPackageLookupValue) | 
|  | containingPackageLookupValues.get(relativePathKey).get(); | 
|  | } catch (NoSuchPackageException e) { | 
|  | logger.warning( | 
|  | "Unexpected exception when looking up containing package for " | 
|  | + relativePath | 
|  | + " (prodaccess expired?): " | 
|  | + e.getMessage()); | 
|  | continue; | 
|  | } | 
|  | if (!containingPackageLookupValue.hasContainingPackage()) { | 
|  | logger.warning(relativePath + " not contained in any package: skipping"); | 
|  | continue; | 
|  | } | 
|  | PathFragment packageFragment = | 
|  | containingPackageLookupValue.getContainingPackageName().getPackageFragment(); | 
|  | String pattern = findFilters.get(i); | 
|  | try { | 
|  | globKeys.add( | 
|  | GlobValue.key( | 
|  | containingPackageLookupValue.getContainingPackageName(), | 
|  | containingPackageLookupValue.getContainingPackageRoot(), | 
|  | pattern, | 
|  | /* excludeDirs= */ true, | 
|  | relativePath.relativeTo(packageFragment))); | 
|  | } catch (InvalidGlobPatternException e) { | 
|  | env.getListener() | 
|  | .handle(Event.warn("Error parsing pattern " + pattern + " for " + relativePath)); | 
|  | continue; | 
|  | } | 
|  | } | 
|  | Map<SkyKey, ValueOrException<IOException>> globResults = | 
|  | env.getValuesOrThrow(globKeys, IOException.class); | 
|  | if (env.valuesMissing()) { | 
|  | return null; | 
|  | } | 
|  | for (Map.Entry<SkyKey, ValueOrException<IOException>> globEntry : globResults.entrySet()) { | 
|  | GlobValue globValue; | 
|  | GlobDescriptor globKey = (GlobDescriptor) globEntry.getKey(); | 
|  | PathFragment packageFragment = globKey.getPackageId().getPackageFragment(); | 
|  | try { | 
|  | globValue = (GlobValue) globEntry.getValue().get(); | 
|  | } catch (IOException e) { | 
|  | logger.warning("Error getting hints for " + packageFragment + ": " + e); | 
|  | continue; | 
|  | } | 
|  | for (PathFragment file : globValue.getMatches()) { | 
|  | hints.add( | 
|  | artifactFactory.getSourceArtifact( | 
|  | packageFragment.getRelative(file), globKey.getPackageRoot())); | 
|  | } | 
|  | } | 
|  | return hints == null || hints.isEmpty() ? ImmutableList.<Artifact>of() : hints; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Performs the work of matching a given path against the hints and returns the expanded paths. | 
|  | * The above {@link #getHintedInclusionsWithSkyframe} should be used in preference, but if the | 
|  | * performance impact of Skyframe restarts is untenable, this can be used as a fallback. | 
|  | */ | 
|  | private Collection<Artifact> getHintedInclusionsLegacy( | 
|  | Rule.Type type, PathFragment path, ArtifactRoot sourceRoot) { | 
|  | String pathString = path.getPathString(); | 
|  | // Delay creation until we know we need one. Use a TreeSet to make sure that the results are | 
|  | // sorted with a stable order and unique. | 
|  | Set<Path> hints = null; | 
|  | for (final Rule rule : rules) { | 
|  | if (type != rule.type) { | 
|  | continue; | 
|  | } | 
|  | Matcher m = rule.pattern.matcher(pathString); | 
|  | if (!m.matches()) { | 
|  | continue; | 
|  | } | 
|  | if (hints == null) { hints = Sets.newTreeSet(); } | 
|  | String relativePath = m.replaceFirst(rule.findRoot); | 
|  | if (!relativePath.startsWith(ALLOWED_PREFIX)) { | 
|  | logger.warning( | 
|  | "Path " | 
|  | + relativePath | 
|  | + " to search after substitution does not start with " | 
|  | + ALLOWED_PREFIX); | 
|  | continue; | 
|  | } | 
|  | Path root = sourceRoot.getRoot().getRelative(relativePath); | 
|  |  | 
|  | if (LOG_FINE) { | 
|  | logger.fine("hint for " + rule.type + " " + pathString + " root: " + root); | 
|  | } | 
|  | try { | 
|  | // The assumption is made here that all files specified by this hint are under the same | 
|  | // package path as the original file -- this filesystem tree traversal is completely | 
|  | // ignorant of package paths. This could be violated if there were a hint that resolved to | 
|  | // foo/**/*.h, there was a package foo/bar, and the packages foo and foo/bar were in | 
|  | // different package paths. In that case, this traversal would fail to pick up | 
|  | // foo/bar/**/*.h. No examples of this currently exist in the INCLUDE_HINTS | 
|  | // file. | 
|  | if (LOG_FINE) { | 
|  | logger.fine("Globbing: " + root + " " + rule.findFilter); | 
|  | } | 
|  | hints.addAll(new UnixGlob.Builder(root) | 
|  | .setFilesystemCalls(syscallCache) | 
|  | .addPattern(rule.findFilter) | 
|  | .glob()); | 
|  | } catch (IOException e) { | 
|  | logger.warning("Error in hint expansion: " + e); | 
|  | } | 
|  | } | 
|  | if (hints != null && !hints.isEmpty()) { | 
|  | // Transform paths into source artifacts (all hints must be to source artifacts). | 
|  | List<Artifact> result = new ArrayList<>(hints.size()); | 
|  | for (Path hint : hints) { | 
|  | Root sourcePath = sourceRoot.getRoot(); | 
|  | result.add( | 
|  | Preconditions.checkNotNull( | 
|  | artifactFactory.getSourceArtifact(sourcePath.relativize(hint), sourcePath), | 
|  | "%s %s %s %s", | 
|  | hint, | 
|  | sourcePath, | 
|  | path)); | 
|  | } | 
|  | return result; | 
|  | } else { | 
|  | return ImmutableList.of(); | 
|  | } | 
|  | } | 
|  |  | 
|  | private Collection<Inclusion> getHintedInclusions(Artifact path) { | 
|  | String pathString = path.getExecPathString(); | 
|  | // Delay creation until we know we need one. Use a LinkedHashSet to make sure that the results | 
|  | // are sorted with a stable order and unique. | 
|  | Set<Inclusion> hints = null; | 
|  | for (final Rule rule : rules) { | 
|  | if ((rule.type != Rule.Type.INCLUDE_ANGLE) && (rule.type != Rule.Type.INCLUDE_QUOTE)) { | 
|  | continue; | 
|  | } | 
|  | Matcher m = rule.pattern.matcher(pathString); | 
|  | if (!m.matches()) { | 
|  | continue; | 
|  | } | 
|  | if (hints == null) { hints = Sets.newLinkedHashSet(); } | 
|  | Inclusion inclusion = new Inclusion(rule.findRoot, rule.type == Rule.Type.INCLUDE_QUOTE | 
|  | ? Kind.QUOTE : Kind.ANGLE); | 
|  | hints.add(inclusion); | 
|  | if (LOG_FINE) { | 
|  | logger.fine("hint for " + rule.type + " " + pathString + " root: " + inclusion); | 
|  | } | 
|  | } | 
|  | if (hints != null && !hints.isEmpty()) { | 
|  | return ImmutableList.copyOf(hints); | 
|  | } else { | 
|  | return ImmutableList.of(); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | Hints getHints() { | 
|  | return hints; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * An immutable inclusion tuple. This models an {@code #include} or {@code | 
|  | * #include_next} line in a file without the context how this file got | 
|  | * included. | 
|  | */ | 
|  | public static class Inclusion { | 
|  | /** The format of the #include in the source file -- quoted, angle bracket, etc. */ | 
|  | enum Kind { | 
|  | /** Quote includes: {@code #include "name"}. */ | 
|  | QUOTE, | 
|  |  | 
|  | /** Angle bracket includes: {@code #include <name>}. */ | 
|  | ANGLE, | 
|  |  | 
|  | /** Quote next includes: {@code #include_next "name"}. */ | 
|  | NEXT_QUOTE, | 
|  |  | 
|  | /** Angle next includes: {@code #include_next <name>}. */ | 
|  | NEXT_ANGLE; | 
|  |  | 
|  | /** | 
|  | * Returns true if this is an {@code #include_next} inclusion, | 
|  | */ | 
|  | boolean isNext() { | 
|  | return this == NEXT_ANGLE || this == NEXT_QUOTE; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** The kind of inclusion. */ | 
|  | final Kind kind; | 
|  | /** The relative path of the inclusion. */ | 
|  | final PathFragment pathFragment; | 
|  |  | 
|  | Inclusion(String includeTarget, Kind kind) { | 
|  | this.kind = kind; | 
|  | this.pathFragment = PathFragment.create(includeTarget); | 
|  | } | 
|  |  | 
|  | Inclusion(PathFragment pathFragment, Kind kind) { | 
|  | this.kind = kind; | 
|  | this.pathFragment = Preconditions.checkNotNull(pathFragment); | 
|  | } | 
|  |  | 
|  | String getPathString() { | 
|  | return pathFragment.getPathString(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public String toString() { | 
|  | return kind + ":" + pathFragment.getPathString(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean equals(Object o) { | 
|  | if (o == this) { | 
|  | return true; | 
|  | } | 
|  | if (!(o instanceof Inclusion)) { | 
|  | return false; | 
|  | } | 
|  | Inclusion that = (Inclusion) o; | 
|  | return kind == that.kind && pathFragment.equals(that.pathFragment); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public int hashCode() { | 
|  | return pathFragment.hashCode() * 37 + kind.hashCode(); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** The externally-scoped immutable hints helper that is shared by all scanners. */ | 
|  | private final Hints hints; | 
|  |  | 
|  | /** | 
|  | * Constructs a new FileParser. | 
|  | * | 
|  | * @param hints regexps for converting computed includes into simple strings | 
|  | */ | 
|  | public IncludeParser(Hints hints) { | 
|  | this.hints = hints; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Skips whitespace, \+NL pairs, and block-style / * * / comments. Assumes | 
|  | * line comments are handled outside. Does not handle digraphs, trigraphs or | 
|  | * decahexagraphs. | 
|  | * | 
|  | * @param chars characters to scan | 
|  | * @param pos the starting position | 
|  | * @return the resulting position after skipping whitespace and comments. | 
|  | */ | 
|  | protected static int skipWhitespace(char[] chars, int pos, int end) { | 
|  | while (pos < end) { | 
|  | if (Character.isWhitespace(chars[pos])) { | 
|  | pos++; | 
|  | } else if (chars[pos] == '\\' && pos + 1 < end && chars[pos + 1] == '\n') { | 
|  | pos++; | 
|  | } else if (chars[pos] == '/' && pos + 1 < end && chars[pos + 1] == '*') { | 
|  | pos += 2; | 
|  | while (pos < end - 1) { | 
|  | if (chars[pos++] == '*') { | 
|  | if (chars[pos] == '/') { | 
|  | pos++; | 
|  | break;  // proper comment end | 
|  | } | 
|  | } | 
|  | } | 
|  | } else {  // not whitespace | 
|  | return pos; | 
|  | } | 
|  | } | 
|  | return pos;  // pos == len, meaning we fell off the end. | 
|  | } | 
|  |  | 
|  | private static final String HAS_INCLUDE = "__has_include"; | 
|  | private static final int HAS_INCLUDE_LENGTH = HAS_INCLUDE.length(); | 
|  | private static final int NECESSARY_HAS_INCLUDE_LENGTH = HAS_INCLUDE_LENGTH + 5; | 
|  |  | 
|  | /** | 
|  | * Returns the index of {@code chars} after the first occurrence of "__has_include" or -1 if no | 
|  | * such occurrence exists. Also requires that there be at least 5 characters after the | 
|  | * "__has_include", corresponding to a pair of parentheses and angle brackets/quotes and a | 
|  | * filename. | 
|  | * | 
|  | * <p>This code runs on every line that starts with " *# *", so it should be as fast as possible. | 
|  | */ | 
|  | private static int skipThroughHasInclude(char[] chars, int pos, int end) { | 
|  | int lastPos = end - NECESSARY_HAS_INCLUDE_LENGTH; | 
|  | while (pos <= lastPos) { | 
|  | int curPos = 0; | 
|  | while (curPos < HAS_INCLUDE_LENGTH && chars[pos + curPos] == HAS_INCLUDE.charAt(curPos)) { | 
|  | curPos++; | 
|  | } | 
|  | if (curPos == HAS_INCLUDE_LENGTH) { | 
|  | return pos + curPos; | 
|  | } | 
|  | // We're looking for "__has_include" as a preprocessing token, which means that it cannot | 
|  | // start in the middle of any characters we've already processed, nor at the mismatching | 
|  | // character. | 
|  | pos += curPos + 1; | 
|  | } | 
|  | return -1; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Checks for and skips a given token. | 
|  | * | 
|  | * @param chars characters to scan | 
|  | * @param pos the starting position | 
|  | * @param expected the expected token | 
|  | * @return the resulting position if found, otherwise -1 | 
|  | */ | 
|  | protected static int expect(char[] chars, int pos, int end, String expected) { | 
|  | int si = 0; | 
|  | int expectedLen = expected.length(); | 
|  | while (pos < end) { | 
|  | if (si == expectedLen) { | 
|  | return pos; | 
|  | } | 
|  | if (chars[pos++] != expected.charAt(si++)) { | 
|  | return -1; | 
|  | } | 
|  | } | 
|  | return -1; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Finds the index of a given character token from a starting pos. | 
|  | * | 
|  | * @param chars characters to scan | 
|  | * @param pos the starting position | 
|  | * @param echar the character to find | 
|  | * @return the resulting position of echar if found, otherwise -1 | 
|  | */ | 
|  | private static int indexOf(char[] chars, int pos, int end, char echar) { | 
|  | while (pos < end) { | 
|  | if (chars[pos] == echar) { | 
|  | return pos; | 
|  | } | 
|  | pos++; | 
|  | } | 
|  | return -1; | 
|  | } | 
|  |  | 
|  | private static final Pattern BS_NL_PAT = Pattern.compile("\\\\" + "\n"); | 
|  |  | 
|  | // Keep this in sync with the grep-includes binary's scanning output format. | 
|  | private static final ImmutableMap<Character, Kind> KIND_MAP = ImmutableMap.of( | 
|  | '"', Kind.QUOTE, | 
|  | '<', Kind.ANGLE, | 
|  | 'q', Kind.NEXT_QUOTE, | 
|  | 'a', Kind.NEXT_ANGLE); | 
|  |  | 
|  | /** | 
|  | * Processes the output generated by an auxiliary include-scanning binary. Closes the stream upon | 
|  | * completion. | 
|  | * | 
|  | * <p>If a source file has the following include statements: | 
|  | * <pre> | 
|  | *   #include <string> | 
|  | *   #include "directory/header.h" | 
|  | * </pre> | 
|  | * | 
|  | * <p>Then the output file has the following contents: | 
|  | * <pre> | 
|  | *   "directory/header.h | 
|  | *   <string | 
|  | * </pre> | 
|  | * <p>Each line of the output is translated into an Inclusion object. | 
|  | */ | 
|  | public static List<Inclusion> processIncludes(Object streamName, InputStream is) | 
|  | throws IOException { | 
|  | List<Inclusion> inclusions = new ArrayList<>(); | 
|  | try (InputStreamReader reader = new InputStreamReader(is, ISO_8859_1)) { | 
|  | for (String line : CharStreams.readLines(reader)) { | 
|  | char qchar = line.charAt(0); | 
|  | String name = line.substring(1); | 
|  | Kind kind = KIND_MAP.get(qchar); | 
|  | if (kind == null) { | 
|  | throw new IOException("Illegal inclusion kind '" + qchar + "'"); | 
|  | } | 
|  | inclusions.add(new Inclusion(name, kind)); | 
|  | } | 
|  | } catch (IOException e) { | 
|  | throw new IOException("Error reading include file " + streamName + ": " + e.getMessage()); | 
|  | } | 
|  | return inclusions; | 
|  | } | 
|  |  | 
|  | @VisibleForTesting | 
|  | Inclusion extractInclusion(String line) { | 
|  | return extractInclusion(line.toCharArray(), 0, line.length()); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Extracts a new, unresolved an Inclusion from a line of source. | 
|  | * | 
|  | * @param chars the char array containing the line chars to parse | 
|  | * @param lineBegin the position of the first character in the line | 
|  | * @param lineEnd the position of the character after the last | 
|  | * @return the inclusion object if possible, null if none | 
|  | */ | 
|  | private Inclusion extractInclusion(char[] chars, int lineBegin, int lineEnd) { | 
|  | // expect WS#WS(include|include_next|__has_include\(_next\)?)WS\(?("name"|<name>|<name>)\)? | 
|  | IncludesKeywordData data = expectIncludeKeyword(chars, lineBegin, lineEnd); | 
|  | int pos = data.pos; | 
|  | if (pos == -1 || pos == lineEnd) { | 
|  | return null; | 
|  | } | 
|  | boolean isNext = false; | 
|  | if (data.canHaveNext) { | 
|  | int npos = expect(chars, pos, lineEnd, "_next"); | 
|  | if (npos >= 0) { | 
|  | isNext = true; | 
|  | pos = npos; | 
|  | } | 
|  | } | 
|  | if ((pos = skipWhitespace(chars, pos, lineEnd)) == lineEnd) { | 
|  | return null; | 
|  | } | 
|  | if (data.hasParens) { | 
|  | if (chars[pos] != '(') { | 
|  | return null; | 
|  | } | 
|  | pos++; | 
|  | if ((pos = skipWhitespace(chars, pos, lineEnd)) == lineEnd) { | 
|  | return null; | 
|  | } | 
|  | } | 
|  | if (chars[pos] == '"' || chars[pos] == '<') { | 
|  | char qchar = chars[pos++]; | 
|  | int spos = pos; | 
|  | pos = indexOf(chars, pos + 1, lineEnd, qchar == '<' ? '>' : '"'); | 
|  | if (pos < 0) { | 
|  | return null; | 
|  | } | 
|  | if (chars[spos] == '/') { | 
|  | return null;  // disallow absolute paths | 
|  | } | 
|  | String name = new String(chars, spos, pos - spos); | 
|  | if (name.contains("\n")) {  // strip any \+NL pairs within name | 
|  | name = BS_NL_PAT.matcher(name).replaceAll(""); | 
|  | } | 
|  | if (isNext) { | 
|  | return new Inclusion(name, qchar == '"' ? Kind.NEXT_QUOTE : Kind.NEXT_ANGLE); | 
|  | } else { | 
|  | return new Inclusion(name, qchar == '"' ? Kind.QUOTE : Kind.ANGLE); | 
|  | } | 
|  | } else { | 
|  | return createOtherInclusion(new String(chars, pos, lineEnd - pos)); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Extracts all inclusions from characters of a file. | 
|  | * | 
|  | * @param chars the file contents to parse & extract inclusions from | 
|  | * @return a new set of inclusions, normalized to the cache | 
|  | */ | 
|  | @VisibleForTesting | 
|  | List<Inclusion> extractInclusions(char[] chars) { | 
|  | List<Inclusion> inclusions = new ArrayList<>(); | 
|  | int lineBegin = 0;  // the first char of each line | 
|  | int end = chars.length;  // the file end | 
|  | while (lineBegin < end) { | 
|  | int lineEnd = lineBegin;   // the char after the last non-\n in each line | 
|  | // skip to the next \n or after end of buffer, ignoring continuations | 
|  | while (lineEnd < end) { | 
|  | if (chars[lineEnd] == '\n') { | 
|  | break; | 
|  | } else if (chars[lineEnd] == '\\') { | 
|  | lineEnd++; | 
|  | if (chars[lineEnd] == '\n') { | 
|  | lineEnd++; | 
|  | } | 
|  | } else { | 
|  | lineEnd++; | 
|  | } | 
|  | } | 
|  |  | 
|  | // TODO(bazel-team) handle multiline block comments /* */ for the cases: | 
|  | //   /* blah blah blah | 
|  | //    lalala  */ #include "foo.h" | 
|  | // and: | 
|  | //   /* blah | 
|  | //   #include "foo.h" | 
|  | //   */ | 
|  |  | 
|  | // extract the inclusion, and save only the kind we care about. | 
|  | Inclusion inclusion = extractInclusion(chars, lineBegin, lineEnd); | 
|  | if (inclusion != null) { | 
|  | if (isValidInclusionKind(inclusion.kind)) { | 
|  | inclusions.add(inclusion); | 
|  | } | 
|  | } | 
|  | lineBegin = lineEnd + 1;  // next line starts after the previous line | 
|  | } | 
|  | return inclusions; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Extracts all inclusions from a given source file. | 
|  | * | 
|  | * @param file the file to parse & extract inclusions from | 
|  | * @param actionExecutionContext Services in the scope of the action, like the stream to which | 
|  | *     scanning messages are printed | 
|  | * @return a new set of inclusions, normalized to the cache | 
|  | */ | 
|  | Collection<Inclusion> extractInclusions( | 
|  | Artifact file, | 
|  | ActionExecutionMetadata actionExecutionMetadata, | 
|  | ActionExecutionContext actionExecutionContext, | 
|  | Artifact grepIncludes, | 
|  | @Nullable SpawnIncludeScanner remoteIncludeScanner, | 
|  | boolean isOutputFile) | 
|  | throws IOException, ExecException, InterruptedException { | 
|  | Collection<Inclusion> inclusions; | 
|  |  | 
|  | if (remoteIncludeScanner != null | 
|  | && remoteIncludeScanner.shouldParseRemotely(file, actionExecutionContext)) { | 
|  | inclusions = | 
|  | remoteIncludeScanner.extractInclusions( | 
|  | file, | 
|  | actionExecutionMetadata, | 
|  | actionExecutionContext, | 
|  | grepIncludes, | 
|  | getFileType(), | 
|  | isOutputFile); | 
|  | } else { | 
|  | try (SilentCloseable c = | 
|  | Profiler.instance().profile(ProfilerTask.SCANNER, file.getExecPathString())) { | 
|  | inclusions = | 
|  | extractInclusions( | 
|  | FileSystemUtils.readContentAsLatin1(actionExecutionContext.getInputPath(file))); | 
|  | } catch (IOException e) { | 
|  | if (remoteIncludeScanner != null) { | 
|  | logger.log( | 
|  | Level.WARNING, | 
|  | "Falling back on remote parsing of " + actionExecutionContext.getInputPath(file), | 
|  | e); | 
|  | inclusions = | 
|  | remoteIncludeScanner.extractInclusions( | 
|  | file, | 
|  | actionExecutionMetadata, | 
|  | actionExecutionContext, | 
|  | grepIncludes, | 
|  | getFileType(), | 
|  | isOutputFile); | 
|  | } else { | 
|  | throw e; | 
|  | } | 
|  | } | 
|  | } | 
|  | if (hints != null) { | 
|  | inclusions.addAll(hints.getHintedInclusions(file)); | 
|  | } | 
|  | return ImmutableList.copyOf(inclusions); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns type of the scanned file. | 
|  | * | 
|  | * <p>Supported values are "c++" for standard c/c++ headers and sources, and "swig" for .swig | 
|  | * files. Changes to this method must be synchronized with change to //tools/cpp:grep-includes. | 
|  | */ | 
|  | protected GrepIncludesFileType getFileType() { | 
|  | return GrepIncludesFileType.CPP; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Position of found include together with information about how to process the remaining include | 
|  | * line further. | 
|  | */ | 
|  | protected static class IncludesKeywordData { | 
|  | protected static final IncludesKeywordData NONE = new IncludesKeywordData(-1, false, false); | 
|  | private final int pos; | 
|  | private final boolean canHaveNext; | 
|  | private final boolean hasParens; | 
|  |  | 
|  | private IncludesKeywordData(int pos, boolean canHaveNext, boolean hasParens) { | 
|  | this.pos = pos; | 
|  | this.canHaveNext = canHaveNext; | 
|  | this.hasParens = hasParens; | 
|  | } | 
|  |  | 
|  | protected static IncludesKeywordData normal(int pos) { | 
|  | return new IncludesKeywordData(pos, true, false); | 
|  | } | 
|  |  | 
|  | protected static IncludesKeywordData importOrSwig(int pos) { | 
|  | return new IncludesKeywordData(pos, false, false); | 
|  | } | 
|  |  | 
|  | protected static IncludesKeywordData hasInclude(int pos) { | 
|  | return new IncludesKeywordData(pos, true, true); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Parses include keyword in the provided char array and returns position immediately after | 
|  | * include keyword or -1 if keyword was not found, along with information to aid future parsing. | 
|  | * Can be overridden by subclasses. | 
|  | */ | 
|  | protected IncludesKeywordData expectIncludeKeyword(char[] chars, int position, int end) { | 
|  | int pos = expect(chars, skipWhitespace(chars, position, end), end, "#"); | 
|  | if (pos > 0) { | 
|  | int npos = skipWhitespace(chars, pos, end); | 
|  | if ((pos = expect(chars, npos, end, "include")) > 0) { | 
|  | return IncludesKeywordData.normal(pos); | 
|  | } else if ((pos = expect(chars, npos, end, "import")) > 0) { | 
|  | return IncludesKeywordData.importOrSwig(pos); | 
|  | } else if ((pos = skipThroughHasInclude(chars, npos, end)) > 0) { | 
|  | return IncludesKeywordData.hasInclude(pos); | 
|  | } | 
|  | } | 
|  | return IncludesKeywordData.NONE; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns true if we interested in the given inclusion kind. Can be overridden by the subclass. | 
|  | * | 
|  | * @param kind | 
|  | */ | 
|  | protected boolean isValidInclusionKind(Kind kind) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns inclusion object for non-standard inclusion cases or null if inclusion should be | 
|  | * ignored. | 
|  | * | 
|  | * @param inclusionContent | 
|  | */ | 
|  | @Nullable | 
|  | protected Inclusion createOtherInclusion(String inclusionContent) { | 
|  | return null; | 
|  | } | 
|  | } |