blob: e06c17778f22a53f99bc279a26d1e7166fbf56b5 [file] [log] [blame]
// Copyright 2025 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.collect;
import static java.util.stream.Collectors.joining;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.vfs.PathFragment;
/**
* A simple matcher for checking whether a given label is part is a set of simple target patterns.
*
* <p>This does not implement full target patterns. Specifically, it handles:
*
* <ul>
* <li>Absolute labels, such as {@code //package:target} or {@code //package/subpackage}
* <li>Absolute package paths and all subpackages and targets, such as {@code //package/...}
* <li>Negative patterns of the above, such as {@code -//package:target}
* </ul>
*
* It does not handle:
*
* <ul>
* <li>Relative labels (all labels with no repository are assumed to be in the main repository)
* <li>The {@code :all} or {@code :*} qualifiers
* </ul>
*
* Patterns are processed in the order given, including negative patterns that override previous
* patterns. This means that if the patterns are
*
* <ul>
* <li>{@code //package/...}
* <li>{@code -//package/subpackage/...}
* <li>{@code //package/subpackage/further/...}
* </ul>
*
* then the labels {@code //package:something}, {@code //package/another} and {@code
* //package/subpackage/further:anything} all match, but the label {@code
* //package/subpackage:something} does not match.
*
* <p>Further note that this class does no loading of BUILD files and performs no verification that
* targets actually exist: it simply matches abstract labels against patterns.
*/
public class SimpleTargetPatternMatcher {
public static SimpleTargetPatternMatcher create(ImmutableList<String> patterns)
throws LabelSyntaxException {
ImmutableList.Builder<SinglePatternMatcher> singlePatternMatcherBuilder =
ImmutableList.builder();
for (String pattern : patterns) {
SinglePatternMatcher matcher = SimpleTargetPatternMatcher.createSinglePatternMatcher(pattern);
singlePatternMatcherBuilder.add(matcher);
}
return new SimpleTargetPatternMatcher(singlePatternMatcherBuilder.build());
}
private final ImmutableList<SinglePatternMatcher> singlePatternMatchers;
private SimpleTargetPatternMatcher(ImmutableList<SinglePatternMatcher> singlePatternMatchers) {
this.singlePatternMatchers = singlePatternMatchers;
}
public boolean isEmpty() {
return this.singlePatternMatchers.isEmpty();
}
/** Returns {@code true} if the label matches all patterns in this matcher. */
public boolean contains(Label label) {
if (this.singlePatternMatchers.isEmpty()) {
return false;
}
// Check each sub-matcher.
MatchResult result = MatchResult.EXCLUDE;
for (SinglePatternMatcher matcher : this.singlePatternMatchers) {
MatchResult matchResult = matcher.matches(label);
if (matchResult == MatchResult.INCLUDE || matchResult == MatchResult.EXCLUDE) {
result = matchResult;
}
}
return result == MatchResult.INCLUDE;
}
@Override
public String toString() {
String joined =
this.singlePatternMatchers.stream()
.map(SinglePatternMatcher::toString)
.collect(joining(","));
return String.format("[%s]", joined);
}
@Override
public boolean equals(Object other) {
if (other instanceof SimpleTargetPatternMatcher otherMatcher) {
return this.singlePatternMatchers.equals(otherMatcher.singlePatternMatchers);
}
return false;
}
@Override
public int hashCode() {
return this.singlePatternMatchers.hashCode();
}
private static SinglePatternMatcher createSinglePatternMatcher(String pattern)
throws LabelSyntaxException {
if (pattern.startsWith("-")) {
// Strip off the leading '-' and create a matcher for what remains. This will technically
// handle a series of nested negative patterns (like `---//exact:target`), but isn't worth
// detecting and throwing an error.
pattern = pattern.substring(1);
SinglePatternMatcher inner = createSinglePatternMatcher(pattern);
return new NegativeMatcher(inner);
} else if (pattern.endsWith("/...")) {
return new WildcardMatcher(pattern);
}
// Just match the pattern as an exact label.
return new ExactMatcher(pattern);
}
private enum MatchResult {
INCLUDE,
EXCLUDE,
NOT_RELEVANT;
}
private sealed interface SinglePatternMatcher
permits ExactMatcher, NegativeMatcher, WildcardMatcher {
MatchResult matches(Label label);
}
/** Checks if the given label exactly matches the pattern. */
private static final class ExactMatcher implements SinglePatternMatcher {
private final String rawPattern;
private final Label label;
private ExactMatcher(String pattern) throws LabelSyntaxException {
this.rawPattern = pattern;
this.label = Label.parseCanonical(pattern);
}
@Override
public MatchResult matches(Label label) {
if (this.label.equals(label)) {
return MatchResult.INCLUDE;
}
return MatchResult.NOT_RELEVANT;
}
@Override
public String toString() {
return this.rawPattern;
}
@Override
public boolean equals(Object other) {
if (other instanceof ExactMatcher otherMatcher) {
return this.rawPattern.equals(otherMatcher.rawPattern);
}
return false;
}
@Override
public int hashCode() {
return this.rawPattern.hashCode();
}
}
/** Checks if the given label fails to match the pattern. */
private static final class NegativeMatcher implements SinglePatternMatcher {
private final SinglePatternMatcher inner;
private NegativeMatcher(SinglePatternMatcher inner) {
this.inner = inner;
}
@Override
public MatchResult matches(Label label) {
return switch (this.inner.matches(label)) {
case INCLUDE -> MatchResult.EXCLUDE;
case EXCLUDE, NOT_RELEVANT -> MatchResult.NOT_RELEVANT;
};
}
@Override
public String toString() {
return String.format("-%s", this.inner);
}
@Override
public boolean equals(Object other) {
if (other instanceof NegativeMatcher otherMatcher) {
return this.inner.equals(otherMatcher.inner);
}
return false;
}
@Override
public int hashCode() {
return 0x37 ^ this.inner.hashCode();
}
}
private static final class WildcardMatcher implements SinglePatternMatcher {
private final PathFragment packagePath;
private WildcardMatcher(String pattern) {
// Strip off the leading "//" and the trailing "/..." and create the wildcard matcher.
this.packagePath = PathFragment.create(pattern.substring(2, pattern.lastIndexOf("...")));
}
@Override
public MatchResult matches(Label label) {
if (label.getPackageFragment().startsWith(this.packagePath)) {
return MatchResult.INCLUDE;
}
return MatchResult.NOT_RELEVANT;
}
@Override
public String toString() {
return String.format("//%s/...", this.packagePath);
}
@Override
public boolean equals(Object other) {
if (other instanceof WildcardMatcher otherMatcher) {
return this.packagePath.equals(otherMatcher.packagePath);
}
return false;
}
@Override
public int hashCode() {
return this.packagePath.hashCode();
}
}
}