/*
* Copyright 2014 Google Inc. 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.errorprone.scanner;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Predicate;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.errorprone.BugCheckerInfo;
import com.google.errorprone.BugPattern;
import com.google.errorprone.BugPattern.SeverityLevel;
import com.google.errorprone.ErrorProneOptions;
import com.google.errorprone.ErrorProneOptions.Severity;
import com.google.errorprone.InvalidCommandLineOptionException;
import com.google.errorprone.bugpatterns.BugChecker;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.annotation.CheckReturnValue;
/**
* Supplies {@link Scanner}s and provides access to the backing sets of all {@link
* BugChecker}s and enabled {@link BugChecker}s.
*/
public abstract class ScannerSupplier implements Supplier<Scanner> {
/* Static factory methods and helpers */
/**
* Returns a {@link ScannerSupplier} with a specific list of {@link BugChecker} classes.
*/
@SafeVarargs
public static ScannerSupplier fromBugCheckerClasses(
Class<? extends BugChecker>... checkerClasses) {
return fromBugCheckerClasses(Arrays.asList(checkerClasses));
}
private static ImmutableMap<String, BugPattern.SeverityLevel> defaultSeverities(
Iterable<BugCheckerInfo> checkers) {
ImmutableMap.Builder<String, BugPattern.SeverityLevel> severities = ImmutableMap.builder();
for (BugCheckerInfo check : checkers) {
severities.put(check.canonicalName(), check.defaultSeverity());
}
return severities.build();
}
/**
* Returns a {@link ScannerSupplier} with a specific list of {@link BugChecker} classes.
*/
public static ScannerSupplier fromBugCheckerClasses(
Iterable<Class<? extends BugChecker>> checkers) {
ImmutableList.Builder<BugCheckerInfo> builder = ImmutableList.builder();
for (Class<? extends BugChecker> checker : checkers) {
builder.add(BugCheckerInfo.create(checker));
}
return fromBugCheckerInfos(builder.build());
}
/**
* Returns a {@link ScannerSupplier} built from a list of {@link BugCheckerInfo}s.
*/
public static ScannerSupplier fromBugCheckerInfos(Iterable<BugCheckerInfo> checkers) {
ImmutableBiMap.Builder<String, BugCheckerInfo> builder = ImmutableBiMap.builder();
for (BugCheckerInfo checker : checkers) {
builder.put(checker.canonicalName(), checker);
}
ImmutableBiMap<String, BugCheckerInfo> allChecks = builder.build();
return new ScannerSupplierImpl(
allChecks, defaultSeverities(allChecks.values()), ImmutableSet.<String>of());
}
/**
* Returns a {@link ScannerSupplier} that just returns the {@link Scanner} that was passed in.
* Used mostly for testing. Does not implement any method other than
* {@link ScannerSupplier#get()}.
*/
public static ScannerSupplier fromScanner(Scanner scanner) {
return new InstanceReturningScannerSupplierImpl(scanner);
}
/* Instance methods */
/**
* Returns a map of check name to {@link BugCheckerInfo} for all {@link BugCheckerInfo}s in this
* {@link ScannerSupplier}, including disabled ones.
*/
@VisibleForTesting
public abstract ImmutableBiMap<String, BugCheckerInfo> getAllChecks();
/**
* Returns the set of {@link BugCheckerInfo}s that are enabled in this {@link ScannerSupplier}.
*/
public abstract ImmutableSet<BugCheckerInfo> getEnabledChecks();
public abstract ImmutableMap<String, BugPattern.SeverityLevel> severities();
protected abstract ImmutableSet<String> disabled();
/**
* Applies options to this {@link ScannerSupplier}.
*
* <p>Command-line options to override check severities may do any of the following:
* <ul>
* <li>Enable a check that is currently off</li>
* <li>Disable a check that is currently on</li>
* <li>Change the severity of a check that is on, promoting a warning to an error or demoting
* an error to a warning</li>
* </ul>
*
* @param errorProneOptions an {@link ErrorProneOptions} object that encapsulates the overrides
* for this compilation
* @throws InvalidCommandLineOptionException if the override map attempts to disable a check
* that may not be disabled
*/
@CheckReturnValue
public ScannerSupplier applyOverrides(ErrorProneOptions errorProneOptions)
throws InvalidCommandLineOptionException {
Map<String, Severity> severityOverrides = errorProneOptions.getSeverityMap();
if (severityOverrides.isEmpty()
&& !errorProneOptions.isEnableAllChecksAsWarnings()
&& !errorProneOptions.isDropErrorsToWarnings()
&& !errorProneOptions.isDisableAllChecks()) {
return this;
}
// Initialize result allChecks map and enabledChecks set with current state of this Supplier.
ImmutableBiMap<String, BugCheckerInfo> checks = getAllChecks();
Map<String, SeverityLevel> severities = new LinkedHashMap<>(severities());
Set<String> disabled = new HashSet<>(disabled());
if (errorProneOptions.isEnableAllChecksAsWarnings()) {
disabled.forEach(c -> severities.put(c, SeverityLevel.WARNING));
disabled.clear();
}
if (errorProneOptions.isDropErrorsToWarnings()) {
getAllChecks()
.values()
.stream()
.filter(
c -> c.defaultSeverity() == SeverityLevel.ERROR && c.suppressibility().disableable())
.forEach(c -> severities.put(c.canonicalName(), SeverityLevel.WARNING));
}
if (errorProneOptions.isDisableAllChecks()) {
getAllChecks()
.values()
.stream()
.filter(c -> c.suppressibility().disableable())
.forEach(c -> disabled.add(c.canonicalName()));
}
// Process overrides
severityOverrides.forEach(
(checkName, newSeverity) -> {
BugCheckerInfo check = getAllChecks().get(checkName);
if (check == null) {
if (errorProneOptions.ignoreUnknownChecks()) {
return;
}
throw new InvalidCommandLineOptionException(checkName + " is not a valid checker name");
}
switch (newSeverity) {
case OFF:
if (!check.suppressibility().disableable()) {
throw new InvalidCommandLineOptionException(
check.canonicalName() + " may not be disabled");
}
severities.remove(check.canonicalName());
disabled.add(check.canonicalName());
break;
case DEFAULT:
severities.put(check.canonicalName(), check.defaultSeverity());
disabled.remove(check.canonicalName());
break;
case WARN:
// Demoting an enabled check from an error to a warning is a form of disabling
if (!disabled().contains(check.canonicalName())
&& !check.suppressibility().disableable()
&& check.defaultSeverity() == SeverityLevel.ERROR) {
throw new InvalidCommandLineOptionException(
check.canonicalName()
+ " is not disableable and may not be demoted to a warning");
}
severities.put(check.canonicalName(), SeverityLevel.WARNING);
disabled.remove(check.canonicalName());
break;
case ERROR:
severities.put(check.canonicalName(), SeverityLevel.ERROR);
disabled.remove(check.canonicalName());
break;
default:
throw new IllegalStateException("Unexpected severity level: " + newSeverity);
}
});
return new ScannerSupplierImpl(
checks, ImmutableMap.copyOf(severities), ImmutableSet.copyOf(disabled));
}
/**
* Composes this {@link ScannerSupplier} with the {@code other}
* {@link ScannerSupplier}. The set of checks that are turned on is the
* intersection of the checks on in {@code this} and {@code other}.
*/
@CheckReturnValue
public ScannerSupplier plus(ScannerSupplier other) {
ImmutableBiMap<String, BugCheckerInfo> combinedAllChecks =
ImmutableBiMap.<String, BugCheckerInfo>builder()
.putAll(this.getAllChecks())
.putAll(other.getAllChecks())
.build();
ImmutableMap<String, SeverityLevel> combinedSeverities =
ImmutableMap.<String, BugPattern.SeverityLevel>builder()
.putAll(severities())
.putAll(other.severities())
.build();
ImmutableSet<String> disabled = ImmutableSet.copyOf(Sets.union(disabled(), other.disabled()));
return new ScannerSupplierImpl(combinedAllChecks, combinedSeverities, disabled);
}
/**
* Filters this {@link ScannerSupplier} based on the provided predicate. Returns a
* {@link ScannerSupplier} with only the checks enabled that satisfy the predicate.
*/
@CheckReturnValue
public ScannerSupplier filter(Predicate<? super BugCheckerInfo> predicate) {
ImmutableSet.Builder<String> disabled = ImmutableSet.builder();
for (BugCheckerInfo check : getAllChecks().values()) {
if (!predicate.apply(check)) {
disabled.add(check.canonicalName());
}
}
return new ScannerSupplierImpl(getAllChecks(), severities(), disabled.build());
}
}