com.google.javascript.jscomp.AllowlistWarningsGuard Maven / Gradle / Ivy
Show all versions of closure-compiler-unshaded Show documentation
/*
* Copyright 2011 The Closure Compiler Authors.
*
* 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.javascript.jscomp;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.common.collect.TreeMultimap;
import com.google.common.io.CharSource;
import com.google.common.io.CharStreams;
import com.google.common.io.Files;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Reader;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import org.jspecify.nullness.Nullable;
/**
* An extension of {@code WarningsGuard} that provides functionality to maintain a list of warnings
* (allowlist). It is subclasses' responsibility to decide what to do with the allowlist by
* implementing the {@code level} function. Warnings are defined by the name of the JS file and the
* first line of warnings description.
*/
@GwtIncompatible("java.io, java.util.regex")
public class AllowlistWarningsGuard extends WarningsGuard {
private static final Splitter LINE_SPLITTER = Splitter.on('\n');
/** The set of allowlisted warnings, same format as {@code formatWarning}. */
private final ImmutableSet allowlist;
/** Pattern to match line number in error descriptions. */
private static final Pattern LINE_NUMBER = Pattern.compile(":-?\\d+");
public AllowlistWarningsGuard() {
this(ImmutableSet.of());
}
/**
* This class depends on an input set that contains the allowlist. The format of each allowlist
* string is: {@code :? } {@code #
* }
*
* @param allowlist The set of JS-warnings that are allowlisted. This is expected to have similar
* format as {@code formatWarning(JSError)}.
*/
public AllowlistWarningsGuard(Set allowlist) {
checkNotNull(allowlist);
this.allowlist = normalizeAllowlist(allowlist);
}
/**
* Loads legacy warnings list from the set of strings. During development line numbers are changed
* very often - we just cut them and compare without ones.
*
* Also remove lines starting with "#" or are blank lines.
*
* @return known legacy warnings without line numbers.
*/
public static ImmutableSet normalizeAllowlist(Set allowlist) {
Set result = new LinkedHashSet<>();
for (String line : allowlist) {
String trimmed = line.trim();
if (trimmed.isEmpty() || trimmed.charAt(0) == '#') {
// strip out empty lines and comments.
continue;
}
// Strip line number for matching.
result.add(LINE_NUMBER.matcher(trimmed).replaceFirst(":"));
}
return ImmutableSet.copyOf(result);
}
@Override
public @Nullable CheckLevel level(JSError error) {
if (error.getDefaultLevel().equals(CheckLevel.ERROR)) {
return null;
}
if (!allowlist.isEmpty() && containWarning(formatWarning(error))) {
// If the message matches the guard we use WARNING, so that it
// - Shows up on stderr, and
// - Gets caught by the AllowlistBuilder downstream in the pipeline
return CheckLevel.WARNING;
}
return null;
}
/**
* Determines whether a given warning is included in the allowlist.
*
* @param formattedWarning the warning formatted by {@code formattedWarning}
* @return whether the given warning is allowlisted or not.
*/
protected boolean containWarning(String formattedWarning) {
return allowlist.contains(formattedWarning);
}
@Override
public int getPriority() {
return WarningsGuard.Priority.SUPPRESS_BY_ALLOWLIST.getValue();
}
/** Creates a warnings guard from a file. */
public static AllowlistWarningsGuard fromFile(File file) {
return new AllowlistWarningsGuard(loadAllowlistedJsWarnings(file));
}
/**
* Loads legacy warnings list from the file.
*
* @return The lines of the file.
*/
public static Set loadAllowlistedJsWarnings(File file) {
return loadAllowlistedJsWarnings(Files.asCharSource(file, UTF_8));
}
/**
* Loads legacy warnings list from the file.
*
* @return The lines of the file.
*/
protected static Set loadAllowlistedJsWarnings(CharSource supplier) {
try {
return loadAllowlistedJsWarnings(supplier.openStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Loads legacy warnings list from the file.
*
* @return The lines of the file.
*/
// TODO(nicksantos): This is a weird API.
static Set loadAllowlistedJsWarnings(Reader reader) throws IOException {
checkNotNull(reader);
Set result = new LinkedHashSet<>();
result.addAll(CharStreams.readLines(reader));
return result;
}
/**
* If subclasses want to modify the formatting, they should override #formatWarning(JSError,
* boolean), not this method.
*/
protected String formatWarning(JSError error) {
return formatWarning(error, false);
}
/**
* @param withMetaData If true, include metadata that's useful to humans This metadata won't be
* used for matching the warning.
*/
protected String formatWarning(JSError error, boolean withMetaData) {
StringBuilder sb = new StringBuilder();
sb.append(normalizeSourceName(error.getSourceName())).append(":");
if (withMetaData) {
sb.append(error.getLineNumber());
}
List lines = LINE_SPLITTER.splitToList(error.getDescription());
sb.append(" ").append(lines.get(0));
// Add the rest of the message as a comment.
if (withMetaData) {
for (int i = 1; i < lines.size(); i++) {
sb.append("\n# ").append(lines.get(i));
}
sb.append("\n");
}
return sb.toString();
}
private String normalizeSourceName(String sourceName) {
if (sourceName != null) {
// e.g.
// "blaze-out/k8-fastbuild/genfiles/some/path/foo.js" -> "some/path/foo.js"
return sourceName.replaceFirst("blaze-out/[^/]*/(bin|genfiles)/", "");
}
return sourceName;
}
public static String getFirstLine(String warning) {
int lineLength = warning.indexOf('\n');
if (lineLength > 0) {
warning = warning.substring(0, lineLength);
}
return warning;
}
/** Allowlist builder */
public class AllowlistBuilder implements ErrorHandler {
private final Set warnings = new LinkedHashSet<>();
private @Nullable String productName = null;
private @Nullable String generatorTarget = null;
private @Nullable String headerNote = null;
/** Fill in your product name to get a fun message! */
@CanIgnoreReturnValue
public AllowlistBuilder setProductName(String name) {
this.productName = name;
return this;
}
/** Fill in instructions on how to generate this allowlist. */
@CanIgnoreReturnValue
public AllowlistBuilder setGeneratorTarget(String name) {
this.generatorTarget = name;
return this;
}
/** A note to include at the top of the allowlist file. */
@CanIgnoreReturnValue
public AllowlistBuilder setNote(String note) {
this.headerNote = note;
return this;
}
@Override
public void report(CheckLevel level, JSError error) {
if (error.getDefaultLevel().equals(CheckLevel.ERROR)) {
// ERROR-level diagnostics are ignored by AllowlistWarningsGuard (c.f. above getLevel).
return;
}
warnings.add(error);
}
/**
* Writes the warnings collected in a format that the AllowlistWarningsGuard can read back
* later.
*/
public void writeAllowlist(File out) throws IOException {
try (PrintStream stream = new PrintStream(out)) {
appendAllowlist(stream);
}
}
/**
* Writes the warnings collected in a format that the AllowlistWarningsGuard can read back
* later.
*/
public void appendAllowlist(PrintStream out) {
out.append("# This is a list of legacy warnings that have yet to be fixed.\n");
if (productName != null && !productName.isEmpty() && !warnings.isEmpty()) {
out.append(
"# Please find some time and fix at least one of them "
+ "and it will be the happiest day for "
+ productName
+ ".\n");
}
if (generatorTarget != null && !generatorTarget.isEmpty()) {
out.append("# When you fix any of these warnings, run " + generatorTarget + " task.\n");
}
if (headerNote != null) {
out.append("#" + Joiner.on("\n# ").join(Splitter.on('\n').split(headerNote)) + "\n");
}
Multimap warningsByType = TreeMultimap.create();
for (JSError warning : warnings) {
warningsByType.put(warning.getType(), formatWarning(warning, true /* withLineNumber */));
}
for (DiagnosticType type : warningsByType.keySet()) {
if (DiagnosticGroups.DEPRECATED.matches(type)) {
// Deprecation warnings are not raisable to error, so we don't need them in allowlists.
continue;
}
out.append("\n# Warning ")
.append(type.key)
.append(": ")
.println(Iterables.get(LINE_SPLITTER.split(type.format), 0));
for (String warning : warningsByType.get(type)) {
out.println(warning);
}
}
out.flush();
}
}
}