blob: b175401c342090904ae7113263725b5a44722a8b [file] [log] [blame]
// Copyright 2014 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.util;
import com.google.common.base.Joiner;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.common.options.Converter;
import com.google.devtools.common.options.OptionsParsingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.annotation.Nullable;
/**
* Handles options that specify list of included/excluded regex expressions. Validates whether
* string is included in that filter.
*
* <p>String is considered to be included into the filter if it does not match any of the excluded
* regex expressions and if it matches at least one included regex expression.
*/
@AutoCodec
@Immutable
public final class RegexFilter {
// Null inclusion or exclusion pattern means those patterns are not used.
@Nullable private final Pattern inclusionPattern;
@Nullable private final Pattern exclusionPattern;
private final int hashCode;
/**
* Converts from a comma-separated list of regex expressions with optional -/+ prefix into the
* RegexFilter. Commas prefixed with backslash are considered to be part of regex definition and
* not a delimiter between separate regex expressions.
*
* <p>Order of expressions is not important. Empty entries are ignored. '-' marks an excluded
* expression.
*/
public static class RegexFilterConverter implements Converter<RegexFilter> {
@Override
public RegexFilter convert(String input) throws OptionsParsingException {
List<String> inclusionList = new ArrayList<>();
List<String> exclusionList = new ArrayList<>();
for (String piece : input.split("(?<!\\\\),")) { // Split on ',' but not on '\,'
piece = piece.replace("\\,", ",");
boolean isExcluded = piece.startsWith("-");
if (isExcluded || piece.startsWith("+")) {
piece = piece.substring(1);
}
if (piece.length() > 0) {
(isExcluded ? exclusionList : inclusionList).add(piece);
}
}
try {
return new RegexFilter(inclusionList, exclusionList);
} catch (PatternSyntaxException e) {
throw new OptionsParsingException("Failed to build valid regular expression: "
+ e.getMessage());
}
}
@Override
public String getTypeDescription() {
return "a comma-separated list of regex expressions with prefix '-' specifying"
+ " excluded paths";
}
}
/**
* Constructor taking regexes directly.
*
* <p>Null {@code inclusionPattern} or {@code exclusionPattern} means that inclusion or exclusion
* matching will not be applied, respectively.
*/
@AutoCodec.Instantiator
RegexFilter(@Nullable Pattern inclusionPattern, @Nullable Pattern exclusionPattern) {
this.inclusionPattern = inclusionPattern;
this.exclusionPattern = exclusionPattern;
this.hashCode =
Objects.hash(
inclusionPattern == null ? null : inclusionPattern.pattern(),
exclusionPattern == null ? null : exclusionPattern.pattern());
}
/** Creates new RegexFilter using provided inclusion and exclusion path lists. */
public RegexFilter(List<String> inclusions, List<String> exclusions) {
this(takeUnionOfRegexes(inclusions), takeUnionOfRegexes(exclusions));
}
/**
* Converts a list of regex expressions into a single regex representing its union or null when
* the list is empty.
*/
private static Pattern takeUnionOfRegexes(List<String> regexList) {
if (regexList.isEmpty()) {
return null;
}
// Wraps each individual regex into an independent group, then combines them using '|' and
// wraps the result in a non-capturing group.
return Pattern.compile("(?:(?>" + Joiner.on(")|(?>").join(regexList) + "))");
}
/**
* @return true iff given string is included (it does not match exclusion pattern (if any) and
* matches inclusionPatter (if any)).
*/
public boolean isIncluded(String value) {
if (exclusionPattern != null && exclusionPattern.matcher(value).find()) {
return false;
}
if (inclusionPattern == null) {
return true;
}
return inclusionPattern.matcher(value).find();
}
@Nullable
public String getInclusionRegex() {
return inclusionPattern == null ? null : inclusionPattern.pattern();
}
@Nullable
public String getExclusionRegex() {
return exclusionPattern == null ? null : exclusionPattern.pattern();
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
if (inclusionPattern != null) {
builder.append(inclusionPattern.pattern().replace(",", "\\,"));
if (exclusionPattern != null) {
builder.append(",");
}
}
if (exclusionPattern != null) {
builder.append("-");
builder.append(exclusionPattern.pattern().replace(",", "\\,"));
}
return builder.toString();
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof RegexFilter)) {
return false;
}
RegexFilter otherFilter = (RegexFilter) other;
if (this.exclusionPattern == null ^ otherFilter.exclusionPattern == null) {
return false;
}
if (this.inclusionPattern == null ^ otherFilter.inclusionPattern == null) {
return false;
}
if (this.exclusionPattern != null && !this.exclusionPattern.pattern().equals(
otherFilter.exclusionPattern.pattern())) {
return false;
}
if (this.inclusionPattern != null && !this.inclusionPattern.pattern().equals(
otherFilter.inclusionPattern.pattern())) {
return false;
}
return true;
}
@Override
public int hashCode() {
return hashCode;
}
}