/*
* 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;
import com.google.auto.value.AutoValue;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.apply.ImportOrganizer;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Processes command-line options specific to error-prone.
*
* <p>Documentation for the available flags are available at http://errorprone.info/docs/flags
*
* @author eaftan@google.com (Eddie Aftandilian)
*/
public class ErrorProneOptions {
private static final String CUSTOM_ENABLEMENT_PREFIX = "-Xep:";
private static final String PATCH_CHECKS_PREFIX = "-XepPatchChecks:";
private static final String PATCH_OUTPUT_LOCATION = "-XepPatchLocation:";
private static final String PATCH_IMPORT_ORDER_PREFIX = "-XepPatchImportOrder:";
private static final String ERRORS_AS_WARNINGS_FLAG = "-XepAllErrorsAsWarnings";
private static final String ENABLE_ALL_CHECKS = "-XepAllDisabledChecksAsWarnings";
private static final String DISABLE_ALL_CHECKS = "-XepDisableAllChecks";
private static final String IGNORE_UNKNOWN_CHECKS_FLAG = "-XepIgnoreUnknownCheckNames";
private static final String DISABLE_WARNINGS_IN_GENERATED_CODE_FLAG =
"-XepDisableWarningsInGeneratedCode";
/**
* see {@link javax.tools.OptionChecker#isSupportedOption(String)}
*/
public static int isSupportedOption(String option) {
boolean isSupported =
option.startsWith(CUSTOM_ENABLEMENT_PREFIX)
|| option.startsWith(PATCH_OUTPUT_LOCATION)
|| option.startsWith(PATCH_CHECKS_PREFIX)
|| option.equals(IGNORE_UNKNOWN_CHECKS_FLAG)
|| option.equals(DISABLE_WARNINGS_IN_GENERATED_CODE_FLAG)
|| option.equals(ERRORS_AS_WARNINGS_FLAG)
|| option.equals(ENABLE_ALL_CHECKS)
|| option.equals(DISABLE_ALL_CHECKS);
return isSupported ? 0 : -1;
}
public boolean isEnableAllChecksAsWarnings() {
return enableAllChecksAsWarnings;
}
public boolean isDisableAllChecks() {
return disableAllChecks;
}
/**
* Severity levels for an error-prone check that define how the check results should be
* presented.
*/
public enum Severity {
DEFAULT, // whatever is specified in the @BugPattern annotation
OFF,
WARN,
ERROR
}
@AutoValue
abstract static class PatchingOptions {
final boolean doRefactor() {
return inPlace() || !baseDirectory().isEmpty();
}
abstract Set<String> namedCheckers();
abstract boolean inPlace();
abstract String baseDirectory();
abstract Optional<Supplier<CodeTransformer>> customRefactorer();
abstract ImportOrganizer importOrganizer();
static Builder builder() {
return new AutoValue_ErrorProneOptions_PatchingOptions.Builder()
.baseDirectory("")
.inPlace(false)
.namedCheckers(Collections.emptySet())
.importOrganizer(ImportOrganizer.STATIC_FIRST_ORGANIZER);
}
@AutoValue.Builder
abstract static class Builder {
abstract Builder namedCheckers(Set<String> checkers);
abstract Builder inPlace(boolean inPlace);
abstract Builder baseDirectory(String baseDirectory);
abstract Builder customRefactorer(Supplier<CodeTransformer> refactorer);
abstract Builder importOrganizer(ImportOrganizer importOrganizer);
abstract PatchingOptions autoBuild();
final PatchingOptions build() {
PatchingOptions patchingOptions = autoBuild();
// If anything is specified, then (checkers or refaster) and output must be set.
if ((!patchingOptions.namedCheckers().isEmpty()
|| patchingOptions.customRefactorer().isPresent())
^ patchingOptions.doRefactor()) {
throw new InvalidCommandLineOptionException(
"-XepPatchChecks and -XepPatchLocation must be specified together");
}
return patchingOptions;
}
}
}
private final ImmutableList<String> remainingArgs;
private final ImmutableMap<String, Severity> severityMap;
private final boolean ignoreUnknownChecks;
private final boolean disableWarningsInGeneratedCode;
private final boolean dropErrorsToWarnings;
private final boolean enableAllChecksAsWarnings;
private final boolean disableAllChecks;
private final PatchingOptions patchingOptions;
private ErrorProneOptions(
ImmutableMap<String, Severity> severityMap,
ImmutableList<String> remainingArgs,
boolean ignoreUnknownChecks,
boolean disableWarningsInGeneratedCode,
boolean dropErrorsToWarnings,
boolean enableAllChecksAsWarnings,
boolean disableAllChecks,
PatchingOptions patchingOptions) {
this.severityMap = severityMap;
this.remainingArgs = remainingArgs;
this.ignoreUnknownChecks = ignoreUnknownChecks;
this.disableWarningsInGeneratedCode = disableWarningsInGeneratedCode;
this.dropErrorsToWarnings = dropErrorsToWarnings;
this.enableAllChecksAsWarnings = enableAllChecksAsWarnings;
this.disableAllChecks = disableAllChecks;
this.patchingOptions = patchingOptions;
}
public String[] getRemainingArgs() {
return remainingArgs.toArray(new String[remainingArgs.size()]);
}
public ImmutableMap<String, Severity> getSeverityMap() {
return severityMap;
}
public boolean ignoreUnknownChecks() {
return ignoreUnknownChecks;
}
public boolean disableWarningsInGeneratedCode() {
return disableWarningsInGeneratedCode;
}
public boolean isDropErrorsToWarnings() {
return dropErrorsToWarnings;
}
public PatchingOptions patchingOptions() {
return patchingOptions;
}
private static class Builder {
private boolean ignoreUnknownChecks = false;
private boolean disableWarningsInGeneratedCode = false;
private boolean dropErrorsToWarnings = false;
private boolean enableAllChecksAsWarnings = false;
private boolean disableAllChecks = false;
private Map<String, Severity> severityMap = new HashMap<>();
private final PatchingOptions.Builder patchingOptionsBuilder = PatchingOptions.builder();
public void setIgnoreUnknownChecks(boolean ignoreUnknownChecks) {
this.ignoreUnknownChecks = ignoreUnknownChecks;
}
public void setDisableWarningsInGeneratedCode(boolean disableWarningsInGeneratedCode) {
this.disableWarningsInGeneratedCode = disableWarningsInGeneratedCode;
}
public void setDropErrorsToWarnings(boolean dropErrorsToWarnings) {
severityMap
.entrySet()
.stream()
.filter(e -> e.getValue() == Severity.ERROR)
.forEach(e -> e.setValue(Severity.WARN));
this.dropErrorsToWarnings = dropErrorsToWarnings;
}
public void putSeverity(String checkName, Severity severity) {
severityMap.put(checkName, severity);
}
public void setEnableAllChecksAsWarnings(boolean enableAllChecksAsWarnings) {
// Checks manually disabled before this flag are reset to warning-level
severityMap
.entrySet()
.stream()
.filter(e -> e.getValue() == Severity.OFF)
.forEach(e -> e.setValue(Severity.WARN));
this.enableAllChecksAsWarnings = enableAllChecksAsWarnings;
}
public void setDisableAllChecks(boolean disableAllChecks) {
// Discard previously set severities so that the DisableAllChecks flag is position sensitive.
severityMap.clear();
this.disableAllChecks = disableAllChecks;
}
public PatchingOptions.Builder patchingOptionsBuilder() {
return patchingOptionsBuilder;
}
public ErrorProneOptions build(ImmutableList<String> outputArgs) {
return new ErrorProneOptions(
ImmutableMap.copyOf(severityMap),
outputArgs,
ignoreUnknownChecks,
disableWarningsInGeneratedCode,
dropErrorsToWarnings,
enableAllChecksAsWarnings,
disableAllChecks,
patchingOptionsBuilder.build());
}
}
private static final ErrorProneOptions EMPTY = new Builder().build(ImmutableList.of());
public static ErrorProneOptions empty() {
return EMPTY;
}
/**
* Given a list of command-line arguments, produce the corresponding {@link ErrorProneOptions}
* instance.
*
* @param args command-line arguments
* @return an {@link ErrorProneOptions} instance encapsulating the given arguments
* @throws InvalidCommandLineOptionException if an error-prone option is invalid
*/
public static ErrorProneOptions processArgs(Iterable<String> args) {
Preconditions.checkNotNull(args);
ImmutableList.Builder<String> outputArgs = ImmutableList.builder();
/* By default, we throw an error when an unknown option is passed in, if for example you
* try to disable a check that doesn't match any of the known checks. This catches typos from
* the command line.
*
* You can pass the IGNORE_UNKNOWN_CHECKS_FLAG to opt-out of that checking. This allows you to
* use command lines from different versions of error-prone interchangably.
*/
Builder builder = new Builder();
for (String arg : args) {
switch (arg) {
case IGNORE_UNKNOWN_CHECKS_FLAG:
builder.setIgnoreUnknownChecks(true);
break;
case DISABLE_WARNINGS_IN_GENERATED_CODE_FLAG:
builder.setDisableWarningsInGeneratedCode(true);
break;
case ERRORS_AS_WARNINGS_FLAG:
builder.setDropErrorsToWarnings(true);
break;
case ENABLE_ALL_CHECKS:
builder.setEnableAllChecksAsWarnings(true);
break;
case DISABLE_ALL_CHECKS:
builder.setDisableAllChecks(true);
break;
default:
if (arg.startsWith(CUSTOM_ENABLEMENT_PREFIX)) {
parseCustomFlagIntoOptionsBuilder(builder, arg);
} else if (arg.startsWith(PATCH_OUTPUT_LOCATION)) {
String remaining = arg.substring(PATCH_OUTPUT_LOCATION.length());
if (remaining.equals("IN_PLACE")) {
builder.patchingOptionsBuilder().inPlace(true);
} else {
if (remaining.isEmpty()) {
throw new InvalidCommandLineOptionException("invalid flag: " + arg);
}
builder.patchingOptionsBuilder().baseDirectory(remaining);
}
} else if (arg.startsWith(PATCH_CHECKS_PREFIX)) {
String remaining = arg.substring(PATCH_CHECKS_PREFIX.length());
if (remaining.startsWith("refaster:")) {
// Refaster rule, load from InputStream at file
builder
.patchingOptionsBuilder()
.customRefactorer(
() -> {
String path = remaining.substring("refaster:".length());
try (InputStream in =
Files.newInputStream(FileSystems.getDefault().getPath(path));
ObjectInputStream ois = new ObjectInputStream(in)) {
return (CodeTransformer) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("Can't load Refaster rule from " + path, e);
}
});
} else {
Iterable<String> checks = Splitter.on(',').trimResults().split(remaining);
builder.patchingOptionsBuilder().namedCheckers(ImmutableSet.copyOf(checks));
}
} else if (arg.startsWith(PATCH_IMPORT_ORDER_PREFIX)) {
String remaining = arg.substring(PATCH_IMPORT_ORDER_PREFIX.length());
ImportOrganizer importOrganizer = ImportOrderParser.getImportOrganizer(remaining);
builder.patchingOptionsBuilder().importOrganizer(importOrganizer);
} else {
outputArgs.add(arg);
}
}
}
return builder.build(outputArgs.build());
}
private static void parseCustomFlagIntoOptionsBuilder(Builder builder, String arg) {
// Strip prefix
String remaining = arg.substring(CUSTOM_ENABLEMENT_PREFIX.length());
// Split on ':'
String[] parts = remaining.split(":");
if (parts.length > 2 || parts[0].isEmpty()) {
throw new InvalidCommandLineOptionException("invalid flag: " + arg);
}
String checkName = parts[0];
Severity severity;
if (parts.length == 1) {
severity = Severity.DEFAULT;
} else { // parts.length == 2
try {
severity = Severity.valueOf(parts[1]);
} catch (IllegalArgumentException e) {
throw new InvalidCommandLineOptionException("invalid flag: " + arg);
}
}
builder.putSeverity(checkName, severity);
}
/**
* Given a list of command-line arguments, produce the corresponding {@link ErrorProneOptions}
* instance.
*
* @param args command-line arguments
* @return an {@link ErrorProneOptions} instance encapsulating the given arguments
* @throws InvalidCommandLineOptionException if an error-prone option is invalid
*/
public static ErrorProneOptions processArgs(String[] args) {
Preconditions.checkNotNull(args);
return processArgs(Arrays.asList(args));
}
}