Add support for the "suppression" lint category.

This commit is contained in:
Archie L. Cobbs 2025-05-10 12:08:49 -05:00
parent 563a143acc
commit 338ba2a1bf
46 changed files with 1708 additions and 193 deletions

View File

@ -47,7 +47,7 @@ $(eval $(call SetupJavaCompilation, BUILD_TOOLS_JDK, \
build/tools/jigsaw \
build/tools/depend, \
BIN := $(BUILDTOOLS_OUTPUTDIR)/jdk_tools_classes, \
DISABLED_WARNINGS := dangling-doc-comments options, \
DISABLED_WARNINGS := dangling-doc-comments options suppression, \
JAVAC_FLAGS := \
--add-exports java.desktop/sun.awt=ALL-UNNAMED \
--add-exports java.base/sun.text=ALL-UNNAMED \
@ -81,7 +81,7 @@ $(eval $(call SetupJavaCompilation, COMPILE_DEPEND, \
SRC := $(TOPDIR)/make/jdk/src/classes, \
INCLUDES := build/tools/depend, \
BIN := $(BUILDTOOLS_OUTPUTDIR)/depend, \
DISABLED_WARNINGS := options, \
DISABLED_WARNINGS := options suppression, \
JAVAC_FLAGS := \
--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \

View File

@ -40,7 +40,7 @@ $(eval $(call SetupJavaCompilation, CLASSLIST_JAR, \
SMALL_JAVA := false, \
SRC := $(TOPDIR)/make/jdk/src/classes, \
INCLUDES := build/tools/classlist, \
DISABLED_WARNINGS := dangling-doc-comments, \
DISABLED_WARNINGS := dangling-doc-comments suppression, \
BIN := $(BUILDTOOLS_OUTPUTDIR)/classlist_classes, \
JAR := $(SUPPORT_OUTPUTDIR)/classlist.jar, \
))

View File

@ -51,7 +51,7 @@ JIMAGE_PKGS := \
# ends up in the image, this will ensure reproducible classes
$(eval $(call SetupJavaCompilation, BUILD_JRTFS, \
COMPILER := interim, \
DISABLED_WARNINGS := options, \
DISABLED_WARNINGS := options suppression, \
TARGET_RELEASE := $(TARGET_RELEASE_JDK8), \
SRC := $(TOPDIR)/src/java.base/share/classes, \
EXCLUDE_FILES := module-info.java, \

View File

@ -28,8 +28,8 @@
# The base module should be built with all warnings enabled. When a
# new warning is added to javac, it can be temporarily added to the
# disabled warnings list.
#
# DISABLED_WARNINGS_java +=
DISABLED_WARNINGS_java += suppression
DOCLINT += -Xdoclint:all/protected \
'-Xdoclint/package:java.*,javax.*'

View File

@ -25,7 +25,7 @@
################################################################################
DISABLED_WARNINGS_java += dangling-doc-comments lossy-conversions this-escape
DISABLED_WARNINGS_java += dangling-doc-comments lossy-conversions this-escape suppression
DOCLINT += -Xdoclint:all/protected \
'-Xdoclint/package:java.*,javax.*'
COPY += .gif .png .wav .txt .xml .css .pf

View File

@ -25,7 +25,7 @@
################################################################################
DISABLED_WARNINGS_java += dangling-doc-comments this-escape
DISABLED_WARNINGS_java += dangling-doc-comments this-escape suppression
DOCLINT += -Xdoclint:all/protected \
'-Xdoclint/package:java.*,javax.*'

View File

@ -25,7 +25,7 @@
################################################################################
DISABLED_WARNINGS_java += dangling-doc-comments this-escape
DISABLED_WARNINGS_java += dangling-doc-comments this-escape suppression
DOCLINT += -Xdoclint:all/protected \
'-Xdoclint/package:java.*,javax.*'

View File

@ -25,6 +25,8 @@
################################################################################
DISABLED_WARNINGS_java += suppression
DOCLINT += -Xdoclint:all/protected \
'-Xdoclint/package:java.*,javax.*'

View File

@ -25,7 +25,7 @@
################################################################################
DISABLED_WARNINGS_java += this-escape
DISABLED_WARNINGS_java += this-escape suppression
DOCLINT += -Xdoclint:all/protected \
'-Xdoclint/package:java.*,javax.*'

View File

@ -25,7 +25,7 @@
################################################################################
DISABLED_WARNINGS_java += dangling-doc-comments
DISABLED_WARNINGS_java += dangling-doc-comments suppression
DOCLINT += -Xdoclint:all/protected \
'-Xdoclint/package:java.*,javax.*'

View File

@ -25,7 +25,7 @@
################################################################################
DISABLED_WARNINGS_java += dangling-doc-comments this-escape
DISABLED_WARNINGS_java += dangling-doc-comments this-escape suppression
DOCLINT += -Xdoclint:all/protected \
'-Xdoclint/package:java.*,javax.*'

View File

@ -25,7 +25,7 @@
################################################################################
DISABLED_WARNINGS_java += dangling-doc-comments this-escape
DISABLED_WARNINGS_java += dangling-doc-comments this-escape suppression
DOCLINT += -Xdoclint:all/protected \
'-Xdoclint/package:java.*,javax.*'

View File

@ -25,7 +25,7 @@
################################################################################
DISABLED_WARNINGS_java += dangling-doc-comments lossy-conversions this-escape
DISABLED_WARNINGS_java += dangling-doc-comments lossy-conversions this-escape suppression
DOCLINT += -Xdoclint:all/protected \
'-Xdoclint/package:$(call CommaList, javax.xml.catalog javax.xml.datatype \
javax.xml.transform javax.xml.validation javax.xml.xpath)'

View File

@ -0,0 +1,30 @@
#
# Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# This code is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 only, as
# published by the Free Software Foundation. Oracle designates this
# particular file as subject to the "Classpath" exception as provided
# by Oracle in the LICENSE file that accompanied this code.
#
# This code is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# version 2 for more details (a copy is included in the LICENSE file that
# accompanied this code).
#
# You should have received a copy of the GNU General Public License version
# 2 along with this work; if not, write to the Free Software Foundation,
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
# or visit www.oracle.com if you need additional information or have any
# questions.
#
################################################################################
DISABLED_WARNINGS_java += suppression
################################################################################

View File

@ -25,6 +25,8 @@
################################################################################
DISABLED_WARNINGS_java += suppression
CLEAN += .properties
################################################################################

View File

@ -25,7 +25,7 @@
################################################################################
DISABLED_WARNINGS_java += dangling-doc-comments this-escape
DISABLED_WARNINGS_java += dangling-doc-comments this-escape suppression
COPY += .properties .caps .txt

View File

@ -25,6 +25,8 @@
################################################################################
DISABLED_WARNINGS_java += suppression
JAVAC_FLAGS += -XDstringConcat=inline
################################################################################

View File

@ -25,6 +25,8 @@
################################################################################
DISABLED_WARNINGS_java += suppression
COPY += .txt
CLEAN_FILES += $(wildcard \

View File

@ -25,7 +25,7 @@
################################################################################
DISABLED_WARNINGS_java += dangling-doc-comments exports
DISABLED_WARNINGS_java += dangling-doc-comments exports suppression
COPY := .xsd .xml .dtd .ini
JAVAC_FLAGS := -XDstringConcat=inline

View File

@ -29,4 +29,6 @@
# upgrade_files_<module-name>.conf files
COPY += .conf
DISABLED_WARNINGS_java += suppression
################################################################################

View File

@ -25,7 +25,7 @@
################################################################################
DISABLED_WARNINGS_java += dangling-doc-comments
DISABLED_WARNINGS_java += dangling-doc-comments suppression
COPY += .gif .png .txt .spec .script .prerm .preinst \
.postrm .postinst .list .sh .desktop .copyright .control .plist .template \

View File

@ -25,6 +25,8 @@
################################################################################
DISABLED_WARNINGS_java += suppression
COPY += .jsh .properties
################################################################################

View File

@ -0,0 +1,30 @@
#
# Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# This code is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 only, as
# published by the Free Software Foundation. Oracle designates this
# particular file as subject to the "Classpath" exception as provided
# by Oracle in the LICENSE file that accompanied this code.
#
# This code is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# version 2 for more details (a copy is included in the LICENSE file that
# accompanied this code).
#
# You should have received a copy of the GNU General Public License version
# 2 along with this work; if not, write to the Free Software Foundation,
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
# or visit www.oracle.com if you need additional information or have any
# questions.
#
################################################################################
DISABLED_WARNINGS_java += suppression
################################################################################

View File

@ -0,0 +1,30 @@
#
# Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# This code is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 only, as
# published by the Free Software Foundation. Oracle designates this
# particular file as subject to the "Classpath" exception as provided
# by Oracle in the LICENSE file that accompanied this code.
#
# This code is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# version 2 for more details (a copy is included in the LICENSE file that
# accompanied this code).
#
# You should have received a copy of the GNU General Public License version
# 2 along with this work; if not, write to the Free Software Foundation,
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
# or visit www.oracle.com if you need additional information or have any
# questions.
#
################################################################################
DISABLED_WARNINGS_java += suppression
################################################################################

View File

@ -25,6 +25,6 @@
################################################################################
DISABLED_WARNINGS_java += this-escape
DISABLED_WARNINGS_java += this-escape suppression
################################################################################

View File

@ -0,0 +1,30 @@
#
# Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# This code is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 only, as
# published by the Free Software Foundation. Oracle designates this
# particular file as subject to the "Classpath" exception as provided
# by Oracle in the LICENSE file that accompanied this code.
#
# This code is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# version 2 for more details (a copy is included in the LICENSE file that
# accompanied this code).
#
# You should have received a copy of the GNU General Public License version
# 2 along with this work; if not, write to the Free Software Foundation,
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
# or visit www.oracle.com if you need additional information or have any
# questions.
#
################################################################################
DISABLED_WARNINGS_java += suppression
################################################################################

View File

@ -47,7 +47,7 @@ $(eval $(call SetupJavaCompilation, BUILD_FAILURE_HANDLER, \
TARGET_RELEASE := $(TARGET_RELEASE_BOOTJDK), \
SRC := $(FH_BASEDIR)/src/share/classes $(FH_BASEDIR)/src/share/conf, \
BIN := $(FH_SUPPORT)/classes, \
DISABLED_WARNINGS := options serial try this-escape, \
DISABLED_WARNINGS := options serial try this-escape suppression, \
COPY := .properties, \
CLASSPATH := $(JTREG_JAR) $(TOOLS_JAR), \
JAR := $(FH_JAR), \

View File

@ -84,7 +84,7 @@ $(eval $(call SetupJavaCompilation, BUILD_JDK_MICROBENCHMARK, \
CLASSPATH := $(JMH_COMPILE_JARS), \
CREATE_API_DIGEST := true, \
DISABLED_WARNINGS := restricted this-escape processing rawtypes removal cast \
serial preview dangling-doc-comments, \
serial preview dangling-doc-comments suppression, \
SRC := $(MICROBENCHMARK_SRC), \
BIN := $(MICROBENCHMARK_CLASSES), \
JAVAC_FLAGS := \

View File

@ -63,6 +63,7 @@ $(eval $(call SetupJavaCompilation, BUILD_TEST_LIB_JAR, \
BIN := $(TEST_LIB_SUPPORT)/test-lib_classes, \
HEADERS := $(TEST_LIB_SUPPORT)/test-lib_headers, \
JAR := $(TEST_LIB_SUPPORT)/test-lib.jar, \
DISABLED_WARNINGS := suppression, \
JAVAC_FLAGS := --add-exports java.base/sun.security.util=ALL-UNNAMED \
--add-exports java.base/jdk.internal.classfile=ALL-UNNAMED \
--add-exports java.base/jdk.internal.classfile.attribute=ALL-UNNAMED \

View File

@ -38,7 +38,6 @@ import com.sun.tools.javac.util.Assert;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;
import com.sun.tools.javac.util.JCDiagnostic.LintWarning;
import com.sun.tools.javac.util.Log;
import com.sun.tools.javac.util.Names;
import com.sun.tools.javac.util.Options;
@ -76,8 +75,9 @@ public class Lint {
*/
public Lint augment(Symbol sym) {
EnumSet<LintCategory> suppressions = suppressionsFrom(sym);
suppressions.removeIf(lc -> !lc.annotationSuppression); // ignore categories that don't support @SuppressWarnings
if (!suppressions.isEmpty()) {
Lint lint = new Lint(this);
Lint lint = new Lint(this, sym);
lint.values.removeAll(suppressions);
lint.suppressedValues.addAll(suppressions);
return lint;
@ -90,7 +90,7 @@ public class Lint {
* @param lc one or more categories to be enabled
*/
public Lint enable(LintCategory... lc) {
Lint l = new Lint(this);
Lint l = new Lint(this, symbol);
l.values.addAll(Arrays.asList(lc));
l.suppressedValues.removeAll(Arrays.asList(lc));
return l;
@ -101,15 +101,14 @@ public class Lint {
* @param lc one or more categories to be suppressed
*/
public Lint suppress(LintCategory... lc) {
Lint l = new Lint(this);
Lint l = new Lint(this, symbol);
l.values.removeAll(Arrays.asList(lc));
l.suppressedValues.addAll(Arrays.asList(lc));
return l;
}
private final Context context;
private final Options options;
private final Log log;
private final LintMapper lintMapper;
// These are initialized lazily to avoid dependency loops
private Symtab syms;
@ -119,22 +118,27 @@ public class Lint {
private EnumSet<LintCategory> values;
private EnumSet<LintCategory> suppressedValues;
private static final Map<String, LintCategory> map = new ConcurrentHashMap<>(20);
// The symbol corresponding to some declaration, or null for the root instance
private final Symbol symbol;
// LintCategory lookup by option string
private static final Map<String, LintCategory> map = new ConcurrentHashMap<>(40);
// Instantiate the root instance
@SuppressWarnings("this-escape")
protected Lint(Context context) {
this.context = context;
context.put(lintKey, this);
options = Options.instance(context);
log = Log.instance(context);
symbol = null;
lintMapper = LintMapper.instance(context);
}
// Instantiate a non-root ("symbol scoped") instance
protected Lint(Lint other) {
other.initializeRootIfNeeded();
protected Lint(Lint other, Symbol symbol) {
Assert.check(symbol != null);
this.symbol = symbol;
this.context = other.context;
this.options = other.options;
this.log = other.log;
this.lintMapper = other.lintMapper;
this.syms = other.syms;
this.names = other.names;
this.values = other.values.clone();
@ -149,6 +153,7 @@ public class Lint {
return;
// Initialize enabled categories based on "-Xlint" flags
Options options = Options.instance(context);
if (options.isSet(Option.XLINT) || options.isSet(Option.XLINT_CUSTOM, "all")) {
// If -Xlint or -Xlint:all is given, enable all categories by default
values = EnumSet.allOf(LintCategory.class);
@ -192,7 +197,11 @@ public class Lint {
@Override
public String toString() {
initializeRootIfNeeded();
return "Lint:[enable" + values + ",suppress" + suppressedValues + "]";
return "Lint["
+ (symbol != null ? "sym=" + symbol : "ROOT")
+ ",enable" + values
+ ",suppress" + suppressedValues
+ "]";
}
/**
@ -293,9 +302,10 @@ public class Lint {
* Warn about issues relating to use of command line options.
*
* <p>
* This category is not supported by {@code @SuppressWarnings}.
* This category is not supported by {@code @SuppressWarnings}
* and is not tracked for unnecessary suppression.
*/
OPTIONS("options", false),
OPTIONS("options", false, false),
/**
* Warn when any output file is written to more than once.
@ -319,9 +329,10 @@ public class Lint {
* Warn about invalid path elements on the command line.
*
* <p>
* This category is not supported by {@code @SuppressWarnings}.
* This category is not supported by {@code @SuppressWarnings}
* and is not tracked for unnecessary suppression.
*/
PATH("path", false),
PATH("path", false, false),
/**
* Warn about issues regarding annotation processing.
@ -363,6 +374,14 @@ public class Lint {
*/
STRICTFP("strictfp"),
/**
* Warn about recognized {@code @SuppressWarnings} lint categories that don't actually suppress any warnings.
*
* <p>
* This category is not tracked for unnecessary suppression.
*/
SUPPRESSION("suppression", true, false),
/**
* Warn about synchronization attempts on instances of @ValueBased classes.
*/
@ -408,8 +427,13 @@ public class Lint {
}
LintCategory(String option, boolean annotationSuppression) {
this(option, annotationSuppression, true);
}
LintCategory(String option, boolean annotationSuppression, boolean suppressionTracking) {
this.option = option;
this.annotationSuppression = annotationSuppression;
this.suppressionTracking = suppressionTracking;
map.put(option, this);
}
@ -432,15 +456,42 @@ public class Lint {
/** Does this category support being suppressed by the {@code @SuppressWarnings} annotation? */
public final boolean annotationSuppression;
/** Does the {@code "suppression"} category track suppressions in this category? */
public final boolean suppressionTracking;
}
/**
* Determine whether warnings in the given category should be calculated, because either
* (a) the category is enabled, or (b) lint category {@code "suppression"} is enabled.
*
* <p>
* Use of this method is never required; it simply helps avoid potentially useless work.
*/
public boolean isActive(LintCategory lc) {
initializeRootIfNeeded();
return values.contains(lc) || needsSuppressionTracking(lc);
}
/**
* Checks if a warning category is enabled. A warning category may be enabled
* on the command line, or by default, and can be temporarily disabled with
* the SuppressWarnings annotation.
*
* <p>
* This method also optionally validates any warning suppressions currently in scope.
* If you just want to know the configuration of this instance, set {@code validate} to false.
* If you are using the result of this method to control whether a warning is actually
* generated, then set {@code validate} to true to ensure that any suppression of the
* category in scope is validated (i.e., determined to actually be suppressing something).
*
* @param lc lint category
* @param validateSuppression true to also validate any suppression of the category
*/
public boolean isEnabled(LintCategory lc) {
public boolean isEnabled(LintCategory lc, boolean validateSuppression) {
initializeRootIfNeeded();
if (validateSuppression)
validateSuppression(lc);
return values.contains(lc);
}
@ -449,9 +500,21 @@ public class Lint {
* of the SuppressWarnings annotation, or, in the case of the deprecated
* category, whether it has been implicitly suppressed by virtue of the
* current entity being itself deprecated.
*
* <p>
* This method also optionally validates any warning suppressions currently in scope.
* If you just want to know the configuration of this instance, set {@code validate} to false.
* If you are using the result of this method to control whether a warning is actually
* generated, then set {@code validate} to true to ensure that any suppression of the
* category in scope is validated (i.e., determined to actually be suppressing something).
*
* @param lc lint category
* @param validateSuppression true to also validate any suppression of the category
*/
public boolean isSuppressed(LintCategory lc) {
public boolean isSuppressed(LintCategory lc, boolean validateSuppression) {
initializeRootIfNeeded();
if (validateSuppression)
validateSuppression(lc);
return suppressedValues.contains(lc);
}
@ -478,7 +541,7 @@ public class Lint {
* @param annotation @SuppressWarnings annotation, or null
* @return set of lint categories, possibly empty but never null
*/
private EnumSet<LintCategory> suppressionsFrom(JCAnnotation annotation) {
public EnumSet<LintCategory> suppressionsFrom(JCAnnotation annotation) {
initializeSymbolsIfNeeded();
if (annotation == null)
return LintCategory.newEmptySet();
@ -504,12 +567,42 @@ public class Lint {
for (Attribute value : values.values) {
Optional.of((String)((Attribute.Constant)value).value)
.flatMap(LintCategory::get)
.filter(lc -> lc.annotationSuppression)
.ifPresent(result::add);
}
return result;
}
/**
* Validate any suppression of the given category currently in scope.
*
* <p>
* Such a suppression will therefore <b>not</b> be declared as unnecessary by the
* {@code "suppression"} warning.
*
* @param lc the lint category to be validated
* @return this instance
*/
public Lint validateSuppression(LintCategory lc) {
if (needsSuppressionTracking(lc))
lintMapper.validateSuppression(symbol, lc);
return this;
}
/**
* Determine whether we should bother tracking suppression validation for the given lint category.
*
* <p>
* We need to track validation of suppression of a lint category if:
* <ul>
* <li>It's supported by {@code "suppression"} suppression tracking
* <li>Category {@code "suppression"} is currently enabled
* </ul>
*/
private boolean needsSuppressionTracking(LintCategory lc) {
initializeRootIfNeeded();
return lc.suppressionTracking && values.contains(LintCategory.SUPPRESSION);
}
private void initializeSymbolsIfNeeded() {
if (syms == null) {
syms = Symtab.instance(context);

View File

@ -26,19 +26,26 @@
package com.sun.tools.javac.code;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.tools.DiagnosticListener;
import javax.tools.JavaFileObject;
import com.sun.tools.javac.code.Lint.LintCategory;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import com.sun.tools.javac.main.Option;
import com.sun.tools.javac.resources.CompilerProperties.LintWarnings;
import com.sun.tools.javac.tree.EndPosTable;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.*;
@ -47,6 +54,13 @@ import com.sun.tools.javac.tree.TreeScanner;
import com.sun.tools.javac.util.Assert;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;
import com.sun.tools.javac.util.Log;
import com.sun.tools.javac.util.Names;
import com.sun.tools.javac.util.Options;
import static com.sun.tools.javac.code.Lint.LintCategory.DEPRECATION;
import static com.sun.tools.javac.code.Lint.LintCategory.OPTIONS;
import static com.sun.tools.javac.code.Lint.LintCategory.SUPPRESSION;
/**
* Maps source code positions to the applicable {@link Lint} instance.
@ -61,10 +75,37 @@ import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;
* The method {@link #lintAt} returns the {@link Lint} instance applicable to source position;
* if it can't be determined yet, an empty {@link Optional} is returned.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
* <p>
* This class also tracks which {@code @SuppressWarnings} suppressions actually suppress something.
* Those that don't are unnecessary and trigger warnings in the {@code "suppression"} lint category.
* For this to work, this class must be notified any time a warning that is currently suppressed would
* have been reported; this is termed the "validation" of the suppression. That notification happens
* via {@link #validateSuppression}.
*
* <p>
* Validation events "bubble up" the source tree until they are "caught" by a {@code @SuppressWarnings}
* annotation or they escape the file entirely. Being "caught" validates that suppression.
* A suppression that is never validated is unnecessary.
*
* <p>
* Additional observations and corner cases:
* <ul>
* <li>Lint warnings can be suppressed at a module, package, class, method, or variable declaration
* (via {@code @SuppressWarnings}), or globally (via {@code -Xlint:-key}).
* <li>Consequently, an unnecessary suppression warning can only be emitted at one of those declarations,
* or globally at the end of compilation (the latter warning is possible in the future).
* <li>Some categories (e.g., {@code classfile}) don't support suppression via {@code @SuppressWarnings}.
* These can only generate warnings at the global level (and therefore any {@code @SuppressWarnings}
* annotation is always unnecessary).
* <li>Some categories are never tracked for suppression, e.g., {@code options}, {@code path}, and the
* suppression category {@code "suppression"} itself.
* <li>{@code @SuppressWarnings("suppression")} is perfectly valid: it means unnecessary suppression
* warnings will never be reported for any lint category suppressed by that annotation or by any
* {@code @SuppressWarnings} annotation nested within the scope of its declaration.
* </ul>
*
* <p><b>This is NOT part of any supported API. If you write code that depends on this, you do so at your
* own risk. This code and its internal interfaces are subject to change or deletion without notice.</b>
*/
public class LintMapper {
@ -74,11 +115,18 @@ public class LintMapper {
// Per-source file lint information
private final Map<JavaFileObject, FileInfo> fileInfoMap = new HashMap<>();
// Validations of "-Xlint:-foo" suppressions
private final EnumSet<LintCategory> optionFlagValidations = LintCategory.newEmptySet();
// Compiler context
private final Context context;
// These are initialized lazily; see initializeIfNeeded()
private Log log;
private Lint rootLint;
private Symtab syms;
private Names names;
private Options options;
/**
* Obtain the {@link LintMapper} context singleton.
@ -101,8 +149,13 @@ public class LintMapper {
// Lazy initialization to avoid dependency loops
private void initializeIfNeeded() {
if (rootLint == null)
if (rootLint == null) {
log = Log.instance(context);
rootLint = Lint.instance(context);
syms = Symtab.instance(context);
names = Names.instance(context);
options = Options.instance(context);
}
}
// Lint Operations
@ -147,6 +200,7 @@ public class LintMapper {
*/
public void clear() {
fileInfoMap.clear();
optionFlagValidations.clear();
}
// Parsing Notifications
@ -167,6 +221,64 @@ public class LintMapper {
fileInfoMap.get(tree.sourcefile).afterParse(tree);
}
// Suppression Tracking
/**
* Validate the given lint category within the scope of the given symbol's declaration (or globally if symbol is null).
*
* <p>
* This is to indicate that, if the category is being suppressed, a warning would have otherwise been generated.
*
* @param symbol innermost {@code @SuppressWarnings}-annotated symbol in scope, or null for global scope
* @param category lint category to validate
*/
public void validateSuppression(Symbol symbol, LintCategory category) {
EnumSet<LintCategory> validations = symbol != null ?
fileInfoMap.get(log.currentSourceFile()).validationsFor(symbol) : optionFlagValidations;
validations.add(category);
}
/**
* Warn about unnecessary {@code @SuppressWarnings} suppressions within the given tree.
*
* <p>
* This step must be done after the given source file has been warned about.
*
* @param sourceFile source file
* @param tree top level declaration
*/
public void reportUnnecessarySuppressionAnnotations(JavaFileObject sourceFile, JCTree tree) {
initializeIfNeeded();
FileInfo fileInfo = fileInfoMap.get(sourceFile);
DeclNode topNode = fileInfo.findTopNode(tree.pos());
// Propagate validations in this top-level declaration to determine which suppressions never got validated
propagateValidations(fileInfo, topNode);
// Report them if needed
if (rootLint.isEnabled(SUPPRESSION, false)) {
topNode.stream()
.filter(node -> node.lint.isEnabled(SUPPRESSION, false))
.forEach(node -> report(node.unvalidated, name -> "\"" + name + "\"",
names -> log.warning(node.annotation.pos(), LintWarnings.UnnecessaryWarningSuppression(names))));
}
}
private void report(EnumSet<LintCategory> unvalidated, Function<String, String> formatter, Consumer<String> logger) {
String names = unvalidated.stream()
.filter(lc -> lc.suppressionTracking)
.map(category -> category.option)
.map(formatter)
.collect(Collectors.joining(", "));
if (!names.isEmpty())
logger.accept(names);
}
// Propagate validations in the given top-level declaration; any that escape validate the corresponding "Xlint" suppression
private void propagateValidations(FileInfo fileInfo, DeclNode topNode) {
optionFlagValidations.addAll(fileInfo.propagateValidations(topNode));
}
// FileInfo
/**
@ -184,8 +296,10 @@ public class LintMapper {
*/
private class FileInfo {
List<Span> topSpans; // the spans of all top level declarations
final DeclNode rootNode = new DeclNode(rootLint); // tree of file's "interesting" declaration nodes
List<Span> topSpans; // the spans of all top level declarations
final DeclNode rootNode = new DeclNode(rootLint); // tree of file's "interesting" declaration nodes
final Map<Symbol, EnumSet<LintCategory>> validationsMap // maps declaration symbol to validations therein
= new HashMap<>();
// Find the Lint that applies to the given position, if known
Optional<Lint> lintAt(DiagnosticPosition pos) {
@ -200,6 +314,25 @@ public class LintMapper {
return Optional.of(node.lint); // use its Lint
}
// Obtain the validation state for the given symbol
EnumSet<LintCategory> validationsFor(Symbol symbol) {
return validationsMap.computeIfAbsent(symbol, s -> LintCategory.newEmptySet());
}
// Combine the validation sets for two variable symbols that are declared together
void mergeValidations(VarSymbol symbol1, VarSymbol symbol2) {
EnumSet<LintCategory> validations1 = validationsFor(symbol1);
EnumSet<LintCategory> validations2 = validationsFor(symbol2);
Assert.check(validations1.equals(validations2));
validationsMap.put(symbol2, validations1); // now the two symbols share the same validation set
}
// Propagate validations in the given top-level node
EnumSet<LintCategory> propagateValidations(DeclNode topNode) {
Assert.check(rootNode.children.contains(topNode));
return topNode.propagateValidations(validationsMap);
}
void afterParse(JCCompilationUnit tree) {
Assert.check(topSpans == null, "source already parsed");
topSpans = tree.defs.stream()
@ -211,7 +344,7 @@ public class LintMapper {
void afterAttr(JCTree tree, EndPosTable endPositions) {
Assert.check(topSpans != null, "source not parsed");
Assert.check(findTopNode(tree.pos()) == null, "duplicate call");
new DeclNodeTreeBuilder(rootNode, endPositions).scan(tree);
new DeclNodeTreeBuilder(this, rootNode, endPositions).scan(tree);
}
Optional<Span> findTopSpan(DiagnosticPosition pos) {
@ -286,6 +419,9 @@ public class LintMapper {
final DeclNode parent; // the immediately containing declaration (null for root)
final List<DeclNode> children = new ArrayList<>(); // the immediately next level down declarations under this node
final Lint lint; // the Lint configuration that applies at this declaration
final JCAnnotation annotation; // the @SuppressWarnings on this declaration, if any
final EnumSet<LintCategory> suppressions; // categories suppressed by @SuppressWarnings, if any
final EnumSet<LintCategory> unvalidated; // categories in "suppressions" that were never validated
// Create a root node representing the entire file
DeclNode(Lint rootLint) {
@ -293,14 +429,21 @@ public class LintMapper {
this.symbol = null;
this.parent = null;
this.lint = rootLint;
this.annotation = null;
this.suppressions = LintCategory.newEmptySet(); // you can't put @SuppressWarnings on a file
this.unvalidated = EnumSet.copyOf(suppressions);
}
// Create a normal declaration node
DeclNode(Symbol symbol, DeclNode parent, JCTree tree, EndPosTable endPositions, Lint lint) {
DeclNode(Symbol symbol, DeclNode parent, JCTree tree, EndPosTable endPositions,
Lint lint, JCAnnotation annotation, EnumSet<LintCategory> suppressions) {
super(tree, endPositions);
this.symbol = symbol;
this.parent = parent;
this.lint = lint;
this.annotation = annotation;
this.suppressions = suppressions;
this.unvalidated = EnumSet.copyOf(suppressions);
parent.children.add(this);
}
@ -318,10 +461,39 @@ public class LintMapper {
return Stream.concat(Stream.of(this), children.stream().flatMap(DeclNode::stream));
}
// Calculate the unvalidated suppressions in the subtree rooted at this node. We do this by recursively
// propagating validations upward until they are "caught" by some matching suppression; this validates
// the suppression. Validations that are not caught are returned to the caller.
public EnumSet<LintCategory> propagateValidations(Map<Symbol, EnumSet<LintCategory>> validationsMap) {
// Recurse on subtrees first and gather their uncaught validations
EnumSet<LintCategory> validations = LintCategory.newEmptySet();
children.stream()
.map(child -> child.propagateValidations(validationsMap))
.forEach(validations::addAll);
// Add in the validations that occurred at this node, if any
Optional.of(symbol)
.map(validationsMap::get)
.ifPresent(validations::addAll);
// Apply (and then discard) validations that match any of this node's suppressions
validations.removeIf(category -> {
if (suppressions.contains(category)) {
unvalidated.remove(category);
return true;
}
return false;
});
// Propagate the remaining validations that weren't caught upward
return validations;
}
@Override
public String toString() {
String label = symbol != null ? "sym=" + symbol : "ROOT";
return String.format("DeclNode[%s,lint=%s]", label, lint);
return String.format("DeclNode[%s,lint=%s,suppressions=%s]", label, lint, suppressions);
}
}
@ -332,12 +504,19 @@ public class LintMapper {
*/
private class DeclNodeTreeBuilder extends TreeScanner {
// Variables declared together (separated by commas) share their @SuppressWarnings annotation, so they must also share
// the set of validated suppressions: the suppression of a category is valid if *any* of the variables validates it.
// We detect that situation using this map and, when found, invoke FileInfo.mergeValidations().
private final Map<JCAnnotation, VarSymbol> annotationRepresentativeSymbolMap = new HashMap<>();
private final FileInfo fileInfo;
private final EndPosTable endPositions;
private DeclNode parent;
private Lint lint;
DeclNodeTreeBuilder(DeclNode rootNode, EndPosTable endPositions) {
DeclNodeTreeBuilder(FileInfo fileInfo, DeclNode rootNode, EndPosTable endPositions) {
this.fileInfo = fileInfo;
this.endPositions = endPositions;
this.parent = rootNode;
this.lint = rootNode.lint; // i.e, rootLint
@ -345,30 +524,30 @@ public class LintMapper {
@Override
public void visitModuleDef(JCModuleDecl tree) {
scanDecl(tree, tree.sym, super::visitModuleDef);
scanDecl(tree, tree.sym, findAnnotation(tree.mods), super::visitModuleDef);
}
@Override
public void visitPackageDef(JCPackageDecl tree) {
scanDecl(tree, tree.packge, super::visitPackageDef);
scanDecl(tree, tree.packge, findAnnotation(tree.annotations), super::visitPackageDef);
}
@Override
public void visitClassDef(JCClassDecl tree) {
scanDecl(tree, tree.sym, super::visitClassDef);
scanDecl(tree, tree.sym, findAnnotation(tree.mods), super::visitClassDef);
}
@Override
public void visitMethodDef(JCMethodDecl tree) {
scanDecl(tree, tree.sym, super::visitMethodDef);
scanDecl(tree, tree.sym, findAnnotation(tree.mods), super::visitMethodDef);
}
@Override
public void visitVarDef(JCVariableDecl tree) {
scanDecl(tree, tree.sym, super::visitVarDef);
scanDecl(tree, tree.sym, findAnnotation(tree.mods), super::visitVarDef);
}
private <T extends JCTree> void scanDecl(T tree, Symbol symbol, Consumer<? super T> recursion) {
private <T extends JCTree> void scanDecl(T tree, Symbol symbol, JCAnnotation annotation, Consumer<? super T> recursion) {
// "symbol" can be null if there were earlier errors; skip this declaration if so
if (symbol == null) {
@ -380,14 +559,27 @@ public class LintMapper {
Lint previousLint = lint;
lint = lint.augment(symbol);
// Get the lint categories explicitly suppressed at this symbol's declaration by @SuppressedWarnings
EnumSet<LintCategory> suppressed = Optional.ofNullable(annotation)
.map(anno -> rootLint.suppressionsFrom(anno))
.orElseGet(LintCategory::newEmptySet);
// Merge validation sets for variables that share the same declaration (and therefore the same @SuppressedWarnings)
if (annotation != null && symbol instanceof VarSymbol varSym) {
annotationRepresentativeSymbolMap.merge(annotation, varSym, (oldSymbol, newSymbol) -> {
fileInfo.mergeValidations(oldSymbol, newSymbol);
return oldSymbol;
});
}
// If this declaration is not "interesting", we don't need to create a DeclNode for it
if (lint == previousLint && parent.parent != null) {
if (lint == previousLint && parent.parent != null && suppressed.isEmpty()) {
recursion.accept(tree);
return;
}
// Add a DeclNode here
DeclNode node = new DeclNode(symbol, parent, tree, endPositions, lint);
DeclNode node = new DeclNode(symbol, parent, tree, endPositions, lint, annotation, suppressed);
parent = node;
try {
recursion.accept(tree);
@ -396,5 +588,23 @@ public class LintMapper {
lint = previousLint;
}
}
// Retrieve the @SuppressWarnings annotation, if any, from the given modifiers
private JCAnnotation findAnnotation(JCModifiers mods) {
return Optional.ofNullable(mods)
.map(m -> m.annotations)
.map(this::findAnnotation)
.orElse(null);
}
// Retrieve the @SuppressWarnings annotation, if any, from the given list of annotations
private JCAnnotation findAnnotation(Collection<JCAnnotation> annotations) {
return Optional.ofNullable(annotations)
.stream()
.flatMap(Collection::stream)
.filter(a -> a.attribute.type.tsym == syms.suppressWarningsType.tsym)
.findFirst()
.orElse(null);
}
}
}

View File

@ -5668,7 +5668,7 @@ public class Attr extends JCTree.Visitor {
// Check for proper use of serialVersionUID and other
// serialization-related fields and methods
if (env.info.lint.isEnabled(LintCategory.SERIAL)
if (env.info.lint.isActive(LintCategory.SERIAL)
&& rs.isSerializable(c.type)
&& !c.isAnonymous()) {
chk.checkSerialStructure(tree, c);

View File

@ -2119,7 +2119,7 @@ public class Check {
private void checkClassOverrideEqualsAndHash(DiagnosticPosition pos,
ClassSymbol someClass) {
if (lint.isEnabled(LintCategory.OVERRIDES)) {
if (lint.isActive(LintCategory.OVERRIDES)) {
MethodSymbol equalsAtObject = (MethodSymbol)syms.objectType
.tsym.members().findFirst(names.equals);
MethodSymbol hashCodeAtObject = (MethodSymbol)syms.objectType
@ -2171,7 +2171,7 @@ public class Check {
public void checkModuleName (JCModuleDecl tree) {
Name moduleName = tree.sym.name;
Assert.checkNonNull(moduleName);
if (lint.isEnabled(LintCategory.MODULE)) {
if (lint.isActive(LintCategory.MODULE)) {
JCExpression qualId = tree.qualId;
while (qualId != null) {
Name componentName;
@ -2642,7 +2642,7 @@ public class Check {
void checkPotentiallyAmbiguousOverloads(JCClassDecl tree, Type site) {
// Skip if warning not enabled
if (!lint.isEnabled(LintCategory.OVERLOADS))
if (!lint.isActive(LintCategory.OVERLOADS))
return;
// Gather all of site's methods, including overridden methods, grouped by name (except Object methods)
@ -2655,10 +2655,6 @@ public class Check {
// Now remove overridden methods from each group, leaving only site's actual members
methodGroups.forEach(list -> removePreempted(list, (m1, m2) -> m1.overrides(m2, site.tsym, types, false)));
// Allow site's own declared methods (only) to apply @SuppressWarnings("overloads")
methodGroups.forEach(list -> list.removeIf(
m -> m.owner == site.tsym && !lint.augment(m).isEnabled(LintCategory.OVERLOADS)));
// Warn about ambiguous overload method pairs for which site is responsible
methodGroups.forEach(list -> compareAndRemove(list, (m1, m2) -> {
@ -2666,6 +2662,16 @@ public class Check {
if (!potentiallyAmbiguousOverload(site, m1, m2) || !responsible.test(m1, m2))
return 0;
// Allow the site's own declared methods (only) to apply @SuppressWarnings("overloads").
// Treat both methods equally so they "share" the validation of the warning suppression,
// but also verify an annotation actually exists on a method before doing that, because
// otherwise we could incorrectly validate an outer annotation.
Predicate<MethodSymbol> methodSuppresses = m -> m.owner == site.tsym &&
m.attribute(syms.suppressWarningsType.tsym) != null &&
lint.augment(m).isSuppressed(LintCategory.OVERLOADS, true);
if (methodSuppresses.test(m1) | methodSuppresses.test(m2)) // use "|" to avoid an artificial preference
return FIRST | SECOND;
// Locate the warning at one of the methods, if possible
DiagnosticPosition pos =
m1.owner == site.tsym ? TreeInfo.diagnosticPositionFor(m1, tree) :
@ -3678,14 +3684,14 @@ public class Check {
}
void checkDeprecatedAnnotation(DiagnosticPosition pos, Symbol s) {
if (lint.isEnabled(LintCategory.DEP_ANN) && s.isDeprecatableViaAnnotation() &&
if (lint.isActive(LintCategory.DEP_ANN) && s.isDeprecatableViaAnnotation() &&
(s.flags() & DEPRECATED) != 0 &&
!syms.deprecatedType.isErroneous() &&
s.attribute(syms.deprecatedType.tsym) == null) {
log.warning(pos, LintWarnings.MissingDeprecatedAnnotation);
}
// Note: @Deprecated has no effect on local variables, parameters and package decls.
if (lint.isEnabled(LintCategory.DEPRECATION) && !s.isDeprecatableViaAnnotation()) {
if (lint.isActive(LintCategory.DEPRECATION) && !s.isDeprecatableViaAnnotation()) {
if (!syms.deprecatedType.isErroneous() && s.attribute(syms.deprecatedType.tsym) != null) {
log.warning(pos, LintWarnings.DeprecatedAnnotationHasNoEffect(Kinds.kindName(s)));
}
@ -4213,7 +4219,7 @@ public class Check {
* Check for a default constructor in an exported package.
*/
void checkDefaultConstructor(ClassSymbol c, DiagnosticPosition pos) {
if (lint.isEnabled(LintCategory.MISSING_EXPLICIT_CTOR) &&
if (lint.isActive(LintCategory.MISSING_EXPLICIT_CTOR) &&
((c.flags() & (ENUM | RECORD)) == 0) &&
!c.isAnonymous() &&
((c.flags() & (PUBLIC | PROTECTED)) != 0) &&
@ -4431,7 +4437,7 @@ public class Check {
Lint prevLint = lint;
try {
lint = lint.augment(tree.sym);
if (lint.isEnabled(LintCategory.EXPORTS)) {
if (lint.isActive(LintCategory.EXPORTS)) {
super.visitMethodDef(tree);
}
} finally {
@ -4445,7 +4451,7 @@ public class Check {
Lint prevLint = lint;
try {
lint = lint.augment(tree.sym);
if (lint.isEnabled(LintCategory.EXPORTS)) {
if (lint.isActive(LintCategory.EXPORTS)) {
scan(tree.mods);
scan(tree.vartype);
}
@ -4464,7 +4470,7 @@ public class Check {
Lint prevLint = lint;
try {
lint = lint.augment(tree.sym);
if (lint.isEnabled(LintCategory.EXPORTS)) {
if (lint.isActive(LintCategory.EXPORTS)) {
scan(tree.mods);
scan(tree.typarams);
try {
@ -4863,6 +4869,14 @@ public class Check {
Lint lint;
// Because runUnderLint() uses "augment" to customize the current Lint instance,
// we must check if the warning category is enabled manually before logging a warning.
private void warning(DiagnosticPosition pos, LintWarning warningKey) {
if (lint.isEnabled(warningKey.getLintCategory(), true)) {
log.warning(pos, warningKey);
}
}
@Override
public Void defaultAction(Element e, JCClassDecl p) {
throw new IllegalArgumentException(Objects.requireNonNullElse(e.toString(), ""));
@ -4894,7 +4908,7 @@ public class Check {
}
if (svuidSym == null) {
log.warning(p.pos(), LintWarnings.MissingSVUID(c));
warning(p.pos(), LintWarnings.MissingSVUID(c));
}
// Check for serialPersistentFields to gate checks for
@ -4921,9 +4935,8 @@ public class Check {
// Note per JLS arrays are
// serializable even if the
// component type is not.
log.warning(
TreeInfo.diagnosticPositionFor(enclosed, tree),
LintWarnings.NonSerializableInstanceField);
warning(TreeInfo.diagnosticPositionFor(enclosed, tree),
LintWarnings.NonSerializableInstanceField);
} else if (varType.hasTag(ARRAY)) {
ArrayType arrayType = (ArrayType)varType;
Type elementType = arrayType.elemtype;
@ -4932,9 +4945,8 @@ public class Check {
elementType = arrayType.elemtype;
}
if (!canBeSerialized(elementType)) {
log.warning(
TreeInfo.diagnosticPositionFor(enclosed, tree),
LintWarnings.NonSerializableInstanceFieldArray(elementType));
warning(TreeInfo.diagnosticPositionFor(enclosed, tree),
LintWarnings.NonSerializableInstanceFieldArray(elementType));
}
}
}
@ -5017,8 +5029,7 @@ public class Check {
}
}
}
log.warning(tree.pos(),
LintWarnings.ExternalizableMissingPublicNoArgCtor);
warning(tree.pos(), LintWarnings.ExternalizableMissingPublicNoArgCtor);
} else {
// Approximate access to the no-arg constructor up in
// the superclass chain by checking that the
@ -5045,8 +5056,8 @@ public class Check {
// Handle nested classes and implicit this$0
(supertype.getNestingKind() == NestingKind.MEMBER &&
((supertype.flags() & STATIC) == 0)))
log.warning(tree.pos(),
LintWarnings.SerializableMissingAccessNoArgCtor(supertype.getQualifiedName()));
warning(tree.pos(),
LintWarnings.SerializableMissingAccessNoArgCtor(supertype.getQualifiedName()));
}
}
}
@ -5064,43 +5075,31 @@ public class Check {
// fields.
if ((svuid.flags() & (STATIC | FINAL)) !=
(STATIC | FINAL)) {
log.warning(
TreeInfo.diagnosticPositionFor(svuid, tree),
LintWarnings.ImproperSVUID((Symbol)e));
warning(TreeInfo.diagnosticPositionFor(svuid, tree), LintWarnings.ImproperSVUID((Symbol)e));
}
// check svuid has type long
if (!svuid.type.hasTag(LONG)) {
log.warning(
TreeInfo.diagnosticPositionFor(svuid, tree),
LintWarnings.LongSVUID((Symbol)e));
warning(TreeInfo.diagnosticPositionFor(svuid, tree), LintWarnings.LongSVUID((Symbol)e));
}
if (svuid.getConstValue() == null)
log.warning(
TreeInfo.diagnosticPositionFor(svuid, tree),
LintWarnings.ConstantSVUID((Symbol)e));
warning(TreeInfo.diagnosticPositionFor(svuid, tree), LintWarnings.ConstantSVUID((Symbol)e));
}
private void checkSerialPersistentFields(JCClassDecl tree, Element e, VarSymbol spf) {
// To be effective, serialPersisentFields must be private, static, and final.
if ((spf.flags() & (PRIVATE | STATIC | FINAL)) !=
(PRIVATE | STATIC | FINAL)) {
log.warning(
TreeInfo.diagnosticPositionFor(spf, tree),
LintWarnings.ImproperSPF);
warning(TreeInfo.diagnosticPositionFor(spf, tree), LintWarnings.ImproperSPF);
}
if (!types.isSameType(spf.type, OSF_TYPE)) {
log.warning(
TreeInfo.diagnosticPositionFor(spf, tree),
LintWarnings.OSFArraySPF);
warning(TreeInfo.diagnosticPositionFor(spf, tree), LintWarnings.OSFArraySPF);
}
if (isExternalizable((Type)(e.asType()))) {
log.warning(
TreeInfo.diagnosticPositionFor(spf, tree),
LintWarnings.IneffectualSerialFieldExternalizable);
warning(TreeInfo.diagnosticPositionFor(spf, tree), LintWarnings.IneffectualSerialFieldExternalizable);
}
// Warn if serialPersistentFields is initialized to a
@ -5110,8 +5109,7 @@ public class Check {
JCVariableDecl variableDef = (JCVariableDecl) spfDecl;
JCExpression initExpr = variableDef.init;
if (initExpr != null && TreeInfo.isNull(initExpr)) {
log.warning(initExpr.pos(),
LintWarnings.SPFNullInit);
warning(initExpr.pos(), LintWarnings.SPFNullInit);
}
}
}
@ -5189,24 +5187,19 @@ public class Check {
private void checkExternMethodRecord(JCClassDecl tree, Element e, MethodSymbol method, Type argType,
boolean isExtern) {
if (isExtern && isExternMethod(tree, e, method, argType)) {
log.warning(
TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.IneffectualExternalizableMethodRecord(method.getSimpleName().toString()));
warning(TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.IneffectualExternalizableMethodRecord(method.getSimpleName().toString()));
}
}
void checkPrivateNonStaticMethod(JCClassDecl tree, MethodSymbol method) {
var flags = method.flags();
if ((flags & PRIVATE) == 0) {
log.warning(
TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.SerialMethodNotPrivate(method.getSimpleName()));
warning(TreeInfo.diagnosticPositionFor(method, tree), LintWarnings.SerialMethodNotPrivate(method.getSimpleName()));
}
if ((flags & STATIC) != 0) {
log.warning(
TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.SerialMethodStatic(method.getSimpleName()));
warning(TreeInfo.diagnosticPositionFor(method, tree), LintWarnings.SerialMethodStatic(method.getSimpleName()));
}
}
@ -5229,18 +5222,14 @@ public class Check {
case FIELD -> {
var field = (VarSymbol)enclosed;
if (serialFieldNames.contains(name)) {
log.warning(
TreeInfo.diagnosticPositionFor(field, tree),
LintWarnings.IneffectualSerialFieldEnum(name));
warning(TreeInfo.diagnosticPositionFor(field, tree), LintWarnings.IneffectualSerialFieldEnum(name));
}
}
case METHOD -> {
var method = (MethodSymbol)enclosed;
if (serialMethodNames.contains(name)) {
log.warning(
TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.IneffectualSerialMethodEnum(name));
warning(TreeInfo.diagnosticPositionFor(method, tree), LintWarnings.IneffectualSerialMethodEnum(name));
}
if (isExtern) {
@ -5278,9 +5267,8 @@ public class Check {
private void checkExternMethodEnum(JCClassDecl tree, Element e, MethodSymbol method, Type argType) {
if (isExternMethod(tree, e, method, argType)) {
log.warning(
TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.IneffectualExternMethodEnum(method.getSimpleName().toString()));
warning(TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.IneffectualExternMethodEnum(method.getSimpleName().toString()));
}
}
@ -5310,9 +5298,7 @@ public class Check {
name = field.getSimpleName().toString();
switch(name) {
case "serialPersistentFields" -> {
log.warning(
TreeInfo.diagnosticPositionFor(field, tree),
LintWarnings.IneffectualSerialFieldInterface);
warning(TreeInfo.diagnosticPositionFor(field, tree), LintWarnings.IneffectualSerialFieldInterface);
}
case "serialVersionUID" -> {
@ -5350,9 +5336,7 @@ public class Check {
Element e,
MethodSymbol method) {
if ((method.flags() & PRIVATE) == 0) {
log.warning(
TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.NonPrivateMethodWeakerAccess);
warning(TreeInfo.diagnosticPositionFor(method, tree), LintWarnings.NonPrivateMethodWeakerAccess);
}
}
@ -5360,9 +5344,7 @@ public class Check {
Element e,
MethodSymbol method) {
if ((method.flags() & DEFAULT) == DEFAULT) {
log.warning(
TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.DefaultIneffective);
warning(TreeInfo.diagnosticPositionFor(method, tree), LintWarnings.DefaultIneffective);
}
}
@ -5407,9 +5389,7 @@ public class Check {
var field = (VarSymbol)enclosed;
switch(name) {
case "serialPersistentFields" -> {
log.warning(
TreeInfo.diagnosticPositionFor(field, tree),
LintWarnings.IneffectualSerialFieldRecord);
warning(TreeInfo.diagnosticPositionFor(field, tree), LintWarnings.IneffectualSerialFieldRecord);
}
case "serialVersionUID" -> {
@ -5431,9 +5411,8 @@ public class Check {
default -> {
if (serialMethodNames.contains(name)) {
log.warning(
TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.IneffectualSerialMethodRecord(name));
warning(TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.IneffectualSerialMethodRecord(name));
}
}}
}}});
@ -5445,9 +5424,8 @@ public class Check {
Element enclosing,
MethodSymbol method) {
if ((method.flags() & (STATIC | ABSTRACT)) != 0) {
log.warning(
TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.SerialConcreteInstanceMethod(method.getSimpleName()));
warning(TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.SerialConcreteInstanceMethod(method.getSimpleName()));
}
}
@ -5462,9 +5440,8 @@ public class Check {
// checking.
Type rtype = method.getReturnType();
if (!types.isSameType(expectedReturnType, rtype)) {
log.warning(
TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.SerialMethodUnexpectedReturnType(method.getSimpleName(),
warning(TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.SerialMethodUnexpectedReturnType(method.getSimpleName(),
rtype, expectedReturnType));
}
}
@ -5478,17 +5455,15 @@ public class Check {
var parameters= method.getParameters();
if (parameters.size() != 1) {
log.warning(
TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.SerialMethodOneArg(method.getSimpleName(), parameters.size()));
warning(TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.SerialMethodOneArg(method.getSimpleName(), parameters.size()));
return;
}
Type parameterType = parameters.get(0).asType();
if (!types.isSameType(parameterType, expectedType)) {
log.warning(
TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.SerialMethodParameterType(method.getSimpleName(),
warning(TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.SerialMethodParameterType(method.getSimpleName(),
expectedType,
parameterType));
}
@ -5507,18 +5482,16 @@ public class Check {
private void checkNoArgs(JCClassDecl tree, Element enclosing, MethodSymbol method) {
var parameters = method.getParameters();
if (!parameters.isEmpty()) {
log.warning(
TreeInfo.diagnosticPositionFor(parameters.get(0), tree),
LintWarnings.SerialMethodNoArgs(method.getSimpleName()));
warning(TreeInfo.diagnosticPositionFor(parameters.get(0), tree),
LintWarnings.SerialMethodNoArgs(method.getSimpleName()));
}
}
private void checkExternalizable(JCClassDecl tree, Element enclosing, MethodSymbol method) {
// If the enclosing class is externalizable, warn for the method
if (isExternalizable((Type)enclosing.asType())) {
log.warning(
TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.IneffectualSerialMethodExternalizable(method.getSimpleName()));
warning(TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.IneffectualSerialMethodExternalizable(method.getSimpleName()));
}
return;
}
@ -5545,10 +5518,8 @@ public class Check {
}
}
if (!declared) {
log.warning(
TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.SerialMethodUnexpectedException(method.getSimpleName(),
thrownType));
warning(TreeInfo.diagnosticPositionFor(method, tree),
LintWarnings.SerialMethodUnexpectedException(method.getSimpleName(), thrownType));
}
}
}
@ -5560,7 +5531,7 @@ public class Check {
try {
lint = lint.augment((Symbol) symbol);
if (lint.isEnabled(LintCategory.SERIAL)) {
if (lint.isActive(LintCategory.SERIAL)) {
task.accept(symbol, p);
}

View File

@ -1352,7 +1352,7 @@ public class Modules extends JCTree.Visitor {
.forEach(result::add);
}
if (lint.isEnabled(LintCategory.INCUBATING)) {
if (lint.isActive(LintCategory.INCUBATING)) {
String incubatingModules = filterAlreadyWarnedIncubatorModules(result.stream()
.filter(msym -> msym.resolutionFlags.contains(ModuleResolutionFlags.WARN_INCUBATING))
.map(msym -> msym.name.toString()))

View File

@ -263,7 +263,7 @@ public class ThisEscapeAnalyzer extends TreeScanner {
Assert.check(methodMap.isEmpty()); // we are not prepared to be used more than once
// Short circuit if this calculation is unnecessary
if (!lintMapper.lintAt(env.toplevel.sourcefile, env.tree.pos()).get().isEnabled(THIS_ESCAPE))
if (!lintMapper.lintAt(env.toplevel.sourcefile, env.tree.pos()).get().isActive(THIS_ESCAPE))
return;
// Determine which packages are exported by the containing module, if any.
@ -343,7 +343,7 @@ public class ThisEscapeAnalyzer extends TreeScanner {
.filter(MethodInfo::analyzable)
.forEach(this::analyzeConstructor);
// Manually apply any Lint suppressions
// Manually apply (and validate) any Lint suppressions
filterWarnings(warning -> !warning.isSuppressed());
// Field intitializers and initialization blocks will generate a separate warning for each primary constructor.
@ -1723,7 +1723,7 @@ public class ThisEscapeAnalyzer extends TreeScanner {
}
boolean isSuppressed() {
return suppressible && !lint().isEnabled(THIS_ESCAPE);
return suppressible && !lint().isEnabled(THIS_ESCAPE, true);
}
int comparePos(StackFrame that) {
@ -1788,12 +1788,12 @@ public class ThisEscapeAnalyzer extends TreeScanner {
}
};
// Determine whether this warning is suppressed. A single "this-escape" warning involves multiple source code
// positions, so we must determine suppression manually. We do this as follows: A warning is suppressed if
// "this-escape" is disabled at any position in the stack where that stack frame corresponds to a constructor
// or field initializer in the target class. That means, for example, @SuppressWarnings("this-escape") annotations
// on regular methods are ignored. We work our way back up the call stack from the point of the leak until we
// encounter a suppressible stack frame.
// Determine whether this warning is suppressed and, if so, validate that suppression. A single "this-escape"
// warning involves multiple source code positions, so we must determine and validate suppression manually.
// We do this as follows: A warning is suppressed if "this-escape" is disabled at any position in the stack
// where that stack frame corresponds to a constructor or field initializer in the target class. That means,
// for example, @SuppressWarnings("this-escape") annotations on regular methods are ignored. We work our way
// back up the call stack from the point of the leak until we encounter a suppressible stack frame.
boolean isSuppressed() {
for (int index = stack.size() - 1; index >= 0; index--) {
if (stack.get(index).isSuppressed())

View File

@ -25,6 +25,7 @@
package com.sun.tools.javac.comp;
import com.sun.tools.javac.code.LintMapper;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Log;
@ -42,6 +43,7 @@ public class WarningAnalyzer {
private final Log log;
private final ThisEscapeAnalyzer thisEscapeAnalyzer;
private final LintMapper lintMapper;
public static WarningAnalyzer instance(Context context) {
WarningAnalyzer instance = context.get(contextKey);
@ -55,9 +57,13 @@ public class WarningAnalyzer {
context.put(contextKey, this);
log = Log.instance(context);
thisEscapeAnalyzer = ThisEscapeAnalyzer.instance(context);
lintMapper = LintMapper.instance(context);
}
public void analyzeTree(Env<AttrContext> env) {
thisEscapeAnalyzer.analyzeTree(env);
// This one should go last
lintMapper.reportUnnecessarySuppressionAnnotations(env.toplevel.sourcefile, env.tree);
}
}

View File

@ -519,7 +519,7 @@ public abstract class BaseFileManager implements JavaFileManager {
synchronized void newOutputToPath(Path path) throws IOException {
// Is output file clash detection enabled?
if (!lint.isEnabled(LintCategory.OUTPUT_FILE_CLASH))
if (!lint.isActive(LintCategory.OUTPUT_FILE_CLASH))
return;
// Get the "canonical" version of the file's path; we are assuming

View File

@ -2246,6 +2246,11 @@ compiler.warn.requires.automatic=\
compiler.warn.requires.transitive.automatic=\
requires transitive directive for an automatic module
# 0: string
# lint: suppression
compiler.warn.unnecessary.warning.suppression=\
unnecessary warning suppression: {0}
# Warnings related to annotation processing
# 0: string
compiler.warn.proc.package.does.not.exist=\

View File

@ -210,6 +210,9 @@ javac.opt.Xlint.desc.empty=\
javac.opt.Xlint.desc.exports=\
Warn about issues regarding module exports.
javac.opt.Xlint.desc.suppression=\
Warn about recognized @SuppressWarnings values that don''t actually suppress any warnings.
javac.opt.Xlint.desc.fallthrough=\
Warn about falling through from one case of a switch statement to the next.

View File

@ -159,21 +159,25 @@ public class Log extends AbstractLog {
public final void reportWithLint(JCDiagnostic diag, Lint lint) {
// Apply hackery for REQUIRES_TRANSITIVE_AUTOMATIC (see also Check.checkModuleRequires())
if (diag.getCode().equals(RequiresTransitiveAutomatic.key()) && !lint.isEnabled(REQUIRES_TRANSITIVE_AUTOMATIC)) {
if (diag.getCode().equals(RequiresTransitiveAutomatic.key()) && !lint.isEnabled(REQUIRES_TRANSITIVE_AUTOMATIC, true)) {
reportWithLint(diags.warning(diag.getDiagnosticSource(), diag.getDiagnosticPosition(), RequiresAutomatic), lint);
return;
}
// Apply the lint configuration (if any) and discard the warning if it gets filtered out
if (lint != null) {
// Gather information
LintCategory category = diag.getLintCategory();
boolean emit = !diag.isFlagSet(DEFAULT_ENABLED) ? // is the warning not enabled by default?
lint.isEnabled(category) : // then emit if the category is enabled
lint.isEnabled(category, false) : // then emit if the category is enabled
category.annotationSuppression ? // else emit if the category is not suppressed, where
!lint.isSuppressed(category) : // ...suppression happens via @SuppressWarnings
!lint.isSuppressed(category, false) : // ...suppression happens via @SuppressWarnings
!options.isSet(XLINT_CUSTOM, "-" + category.option); // ...suppression happens via -Xlint:-category
if (!emit)
if (!emit) {
validateSuppression(new SuppressionValidation(lint, diag)); // validate any suppression
return;
}
}
// Proceed
@ -185,6 +189,11 @@ public class Log extends AbstractLog {
*/
protected abstract void reportReady(JCDiagnostic diag);
/**
* Validate a lint suppression.
*/
protected abstract void validateSuppression(SuppressionValidation validation);
protected void addLintWaiter(JavaFileObject sourceFile, JCDiagnostic diagnostic) {
lintWaitersMap.computeIfAbsent(sourceFile, s -> new LinkedList<>()).add(diagnostic);
}
@ -220,6 +229,12 @@ public class Log extends AbstractLog {
return diagnosticList.isEmpty();
});
}
protected record SuppressionValidation(Lint lint, JCDiagnostic diag) {
void validate() {
lint.validateSuppression(diag.getLintCategory());
}
}
}
/**
@ -232,6 +247,9 @@ public class Log extends AbstractLog {
@Override
protected void reportReady(JCDiagnostic diag) { }
@Override
protected void validateSuppression(SuppressionValidation validation) { }
}
/**
@ -243,6 +261,7 @@ public class Log extends AbstractLog {
*/
public class DeferredDiagnosticHandler extends DiagnosticHandler {
private List<JCDiagnostic> deferred = new ArrayList<>();
private List<SuppressionValidation> validatedSuppressions = new ArrayList<>();
private final Predicate<JCDiagnostic> filter;
private final boolean passOnNonDeferrable;
@ -281,6 +300,15 @@ public class Log extends AbstractLog {
}
}
@Override
protected void validateSuppression(SuppressionValidation validation) {
if (deferrable(validation.diag)) {
validatedSuppressions.add(validation);
} else {
prev.validateSuppression(validation);
}
}
public List<JCDiagnostic> getDiagnostics() {
return deferred;
}
@ -305,6 +333,12 @@ public class Log extends AbstractLog {
.filter(accepter)
.forEach(diagnostic -> prev.addLintWaiter(sourceFile, diagnostic)));
lintWaitersMap = null; // prevent accidental ongoing use
// Flush matching suppression validations to the previous handler
validatedSuppressions.stream()
.filter(vs -> accepter.test(vs.diag))
.forEach(prev::validateSuppression);
validatedSuppressions = null; // prevent accidental ongoing use
}
/** Report all deferred diagnostics in the specified order. */
@ -943,9 +977,14 @@ public class Log extends AbstractLog {
// Apply the appropriate mandatory warning aggregator, if needed
if (diagnostic.isFlagSet(AGGREGATE)) {
LintCategory category = diagnostic.getLintCategory();
boolean verbose = lintFor(diagnostic).isEnabled(category);
if (!aggregatorFor(category).aggregate(diagnostic, verbose))
Lint lint = lintFor(diagnostic);
boolean verbose = lint.isEnabled(category, false);
if (!aggregatorFor(category).aggregate(diagnostic, verbose)) {
// Aggregation effectively suppresses the warning, so validate that suppression
validateSuppression(new SuppressionValidation(lint, diagnostic));
return;
}
}
// Emit warning unless not mandatory and warnings are disabled
@ -974,6 +1013,11 @@ public class Log extends AbstractLog {
compressedOutput = true;
}
}
@Override
protected void validateSuppression(SuppressionValidation validation) {
validation.validate();
}
}
/**

View File

@ -186,6 +186,7 @@ import javax.tools.StandardLocation;
* and interfaces
* <tr><th scope="row">{@code static} <td>accessing a static member using an instance
* <tr><th scope="row">{@code strictfp} <td>unnecessary use of the {@code strictfp} modifier
* <tr><th scope="row">{@code suppression} <td>unnecessary suppressions in {@code @SuppressWarnings} annotations
* <tr><th scope="row">{@code synchronization} <td>synchronization attempts on instances of value-based classes
* <tr><th scope="row">{@code text-blocks} <td>inconsistent white space characters in text block indentation
* <tr><th scope="row">{@code this-escape} <td>superclass constructor leaking {@code this} before subclass initialized

View File

@ -645,6 +645,9 @@ file system locations may be directories, JAR files or JMOD files.
- `strictfp`: Warns about unnecessary use of the `strictfp` modifier.
- `suppression`: Warns about recognized `@SuppressWarnings` values that
don't actually suppress any warnings.
- `synchronization`: Warns about synchronization attempts on instances
of value-based classes.

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
// key: compiler.warn.unnecessary.warning.suppression
// options: -Xlint:suppression
@SuppressWarnings("unchecked")
class X {
}

View File

@ -0,0 +1,981 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/**
* @test
* @bug 8344159
* @summary Test "suppression" lint warnings
* @library /tools/lib
* @modules
* jdk.compiler/com.sun.tools.javac.api
* jdk.compiler/com.sun.tools.javac.code
* jdk.compiler/com.sun.tools.javac.main
* @build toolbox.ToolBox toolbox.JavacTask toolbox.JarTask
* @run main SuppressionWarningTest
*/
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.sun.tools.javac.code.Lint;
import com.sun.tools.javac.code.Lint.LintCategory;
import com.sun.tools.javac.code.Source;
import toolbox.JarTask;
import toolbox.JavacTask;
import toolbox.Task.Mode;
import toolbox.Task;
import toolbox.TestRunner;
import toolbox.ToolBox;
import static com.sun.tools.javac.code.Lint.LintCategory.*;
public class SuppressionWarningTest extends TestRunner {
// Test cases for testSuppressWarnings()
public static final List<SuppressTest> SUPPRESS_WARNINGS_TEST_CASES = Stream.of(LintCategory.values())
.filter(category -> category.suppressionTracking)
.map(category -> switch (category) {
case AUXILIARYCLASS -> new SuppressTest(category,
"compiler.warn.auxiliary.class.accessed.from.outside.of.its.source.file",
null,
"""
public class Class1 { }
class AuxClass { }
""",
"""
@OUTER@
public class Class2 {
@INNER@
public Object obj = new AuxClass();
}
"""
);
case CAST -> new SuppressTest(category,
"compiler.warn.redundant.cast",
null,
"""
@OUTER@
public class Test {
@INNER@
public Object obj = (Object)new Object();
}
"""
);
case CLASSFILE -> null; // skip, too hard to simluate
case DANGLING_DOC_COMMENTS -> new SuppressTest(category,
"compiler.warn.dangling.doc.comment",
null,
"""
@OUTER@
public class Test {
/** Dangling comment */
/** Javadoc comment */
@INNER@
public void foo() {
}
}
"""
);
case DEPRECATION -> new SuppressTest(category,
"compiler.warn.has.been.deprecated",
null,
"""
public class Super {
@Deprecated
public void foo() { }
}
""",
"""
@OUTER@
public class Sub extends Super {
@INNER@
@Override
public void foo() { }
}
"""
);
case DEP_ANN -> new SuppressTest(category,
"compiler.warn.missing.deprecated.annotation",
null,
"""
@OUTER@
public class Test {
@INNER@
public class TestSub {
/** @deprecated */
public void method() { }
}
}
"""
);
case DIVZERO -> new SuppressTest(category,
"compiler.warn.div.zero",
null,
"""
@OUTER@
public class Test {
@INNER@
public int method() {
return 1/0;
}
}
"""
);
case EMPTY -> new SuppressTest(category,
"compiler.warn.empty.if",
null,
"""
@OUTER@
public class Test {
@INNER@
public void method(boolean x) {
if (x);
}
}
"""
);
case EXPORTS -> new SuppressTest(category,
"compiler.warn.leaks.not.accessible",
null,
"""
module mod {
exports pkg1;
}
""",
"""
// @MODULE@:mod
package pkg1;
@OUTER@
public class Class1 {
@INNER@
public pkg2.Class2 obj2; // warning here
}
""",
"""
// @MODULE@:mod
package pkg2;
public class Class2 {
}
"""
);
case FALLTHROUGH -> new SuppressTest(category,
"compiler.warn.possible.fall-through.into.case",
null,
"""
@OUTER@
public class Test {
@INNER@
public void method(int x) {
switch (x) {
case 1:
System.out.println(1);
default:
System.out.println(0);
}
}
}
"""
);
case FINALLY -> new SuppressTest(category,
"compiler.warn.finally.cannot.complete",
null,
"""
@OUTER@
public class Test {
@INNER@
public void method(int x) {
try {
System.out.println(x);
} finally {
throw new RuntimeException();
}
}
}
"""
);
case INCUBATING -> null; // skip, too hard to simluate reliably over time
case LOSSY_CONVERSIONS -> new SuppressTest(category,
"compiler.warn.possible.loss.of.precision",
null,
"""
@OUTER@
public class Test {
@INNER@
public void method() {
long b = 1L;
b += 0.1 * 3L;
}
}
"""
);
case MISSING_EXPLICIT_CTOR -> new SuppressTest(category,
"compiler.warn.missing-explicit-ctor",
null,
"""
module mod {
exports pkg1;
}
""",
"""
package pkg1;
@OUTER@
public class Class1 {
public Class1(int x) {
}
@INNER@
public static class Sub {
}
}
"""
);
case MODULE -> new SuppressTest(category,
"compiler.warn.poor.choice.for.module.name",
null,
"""
@OUTER@
module mod0 {
}
"""
);
case OPENS -> new SuppressTest(category,
"compiler.warn.package.empty.or.not.found",
null,
"""
@OUTER@
module mod {
opens pkg1;
}
"""
);
// This test case only works on MacOS
case OUTPUT_FILE_CLASH ->
System.getProperty("os.name").startsWith("Mac") ?
new SuppressTest(category,
"compiler.warn.output.file.clash",
null,
"""
@OUTER@
public class Test {
interface Cafe\u0301 { // macos normalizes "e" + U0301 -> U00e9
}
interface Caf\u00e9 {
}
}
"""
) : null;
case OVERLOADS -> new SuppressTest(category,
"compiler.warn.potentially.ambiguous.overload",
null,
"""
import java.util.function.*;
@OUTER@
public class Super {
public void foo(IntConsumer c) {
}
@INNER@
public void foo(Consumer<Integer> c) {
}
}
"""
);
case OVERRIDES -> new SuppressTest(category,
"compiler.warn.override.equals.but.not.hashcode",
null,
"""
@OUTER@
public class Test {
@INNER@
public class Test2 {
public boolean equals(Object obj) {
return false;
}
}
}
"""
);
case PROCESSING -> null; // skip for now
case RAW -> new SuppressTest(category,
"compiler.warn.raw.class.use",
null,
"""
@OUTER@
public class Test {
@INNER@
public void foo() {
Iterable i = null;
}
}
"""
);
case REMOVAL -> new SuppressTest(category,
"compiler.warn.has.been.deprecated.for.removal",
null,
"""
public class Super {
@Deprecated(forRemoval = true)
public void foo() { }
}
""",
"""
@OUTER@
public class Sub extends Super {
@INNER@
@Override
public void foo() { }
}
"""
);
// This test case requires special support; see testSuppressWarnings()
case REQUIRES_AUTOMATIC -> new SuppressTest(category,
"compiler.warn.requires.automatic",
null,
"""
@OUTER@
module m1x {
requires randomjar;
}
"""
);
// This test case requires special support; see testSuppressWarnings()
case REQUIRES_TRANSITIVE_AUTOMATIC -> new SuppressTest(category,
"compiler.warn.requires.transitive.automatic",
null,
"""
@OUTER@
module m1x {
requires transitive randomjar;
}
"""
);
case SERIAL -> new SuppressTest(category,
"compiler.warn.missing.SVUID",
null,
"""
@OUTER@
public class Test {
@INNER@
public static class Inner implements java.io.Serializable {
public int x;
}
}
"""
);
case STATIC -> new SuppressTest(category,
"compiler.warn.static.not.qualified.by.type",
null,
"""
@OUTER@
public class Test {
public static void foo() {
}
@INNER@
public void bar() {
this.foo();
}
}
"""
);
case STRICTFP -> new SuppressTest(category,
"compiler.warn.strictfp",
null,
"""
@OUTER@
public class Test {
@INNER@
public strictfp void foo() {
}
}
"""
);
case SYNCHRONIZATION -> new SuppressTest(category,
"compiler.warn.attempt.to.synchronize.on.instance.of.value.based.class",
null,
"""
@OUTER@
public class Outer {
@INNER@
public void foo() {
Integer i = 42;
synchronized (i) {
}
}
}
"""
);
case TEXT_BLOCKS -> new SuppressTest(category,
"compiler.warn.trailing.white.space.will.be.removed",
null,
"""
@OUTER@
public class Test {
public void foo() {
String s =
\"\"\"
add trailing spaces here:
\"\"\";
}
}
""".replaceAll("add trailing spaces here:", "$0 ")
);
case THIS_ESCAPE -> new SuppressTest(category,
"compiler.warn.possible.this.escape",
null,
"""
@OUTER@
public class Outer {
@INNER@
public static class Inner {
public Inner() {
leak();
}
public void leak() { }
}
}
"""
);
case TRY -> new SuppressTest(category,
"compiler.warn.try.explicit.close.call",
null,
"""
import java.io.*;
@OUTER@
public class Outer {
@INNER@
public void foo() throws IOException {
try (InputStream in = new FileInputStream("x")) {
in.close();
}
}
}
"""
);
case UNCHECKED -> new SuppressTest(category,
"compiler.warn.prob.found.req: (compiler.misc.unchecked.cast.to.type)",
null,
"""
@OUTER@
public class Test {
public void foo() {
Iterable<?> c = null;
@INNER@
Iterable<Short> t = (Iterable<Short>)c, s = null;
}
}
"""
);
case VARARGS -> new SuppressTest(category,
"compiler.warn.varargs.unsafe.use.varargs.param",
null,
"""
@OUTER@
public class Test {
@INNER@
@SafeVarargs
public static <T> void bar(final T... barArgs) {
baz(barArgs);
}
public static <T> void baz(final T[] bazArgs) {
}
}
"""
);
// This test case requires special support; see testSuppressWarnings()
case PREVIEW -> new SuppressTest(category,
"compiler.warn.preview.feature.use",
new String[] {
"--enable-preview",
"-XDforcePreview"
},
"""
@OUTER@
public class Test {
@INNER@
public Test(Object x) {
int value = x instanceof Integer i ? i : -1;
}
}
"""
);
case RESTRICTED -> new SuppressTest(category,
"compiler.warn.restricted.method",
null,
"""
@OUTER@
public class Test {
@INNER@
public void foo() {
System.load("");
}
}
"""
);
default -> throw new AssertionError("missing test case for " + category);
})
.filter(Objects::nonNull) // skip categories with no test case defined
.collect(Collectors.toList());
protected final ToolBox tb;
public SuppressionWarningTest() {
super(System.err);
tb = new ToolBox();
}
public static void main(String... args) throws Exception {
SuppressionWarningTest test = new SuppressionWarningTest();
// Run parameterized tests
test.runTestsMulti(m -> switch (m.getName()) {
case "testSuppressWarnings" -> SUPPRESS_WARNINGS_TEST_CASES.stream()
.map(testCase -> new Object[] { testCase });
case "testUselessAnnotation" -> Stream.of(LintCategory.values())
.filter(category -> category.suppressionTracking)
.map(category -> new Object[] { category });
case "testSelfSuppression" -> Stream.of(RAW, SUPPRESSION)
.map(category -> new Object[] { category });
case "testOverloads" -> Stream.<Object[]>of(new Object[0]); // no parameters for this test
case "testThisEscape" -> Stream.<Object[]>of(new Object[0]); // no parameters for this test
default -> throw new AssertionError("missing params for " + m);
});
}
// We are testing all combinations of nested @SuppressWarning annotations and lint flags
@Test
public void testSuppressWarnings(SuppressTest test) throws Exception {
// Setup directories
Path base = Paths.get("testSuppressWarnings");
resetCompileDirectories(base);
// Detect if any modules are being compiled; if so we need to create an extra source directory level
Pattern moduleDecl = Pattern.compile("module\\s+(\\S*).*");
Set<String> moduleNames = test.sources().stream()
.flatMap(source -> Stream.of(source.split("\\n")))
.map(moduleDecl::matcher)
.filter(Matcher::matches)
.map(matcher -> matcher.group(1))
.collect(Collectors.toSet());
// Special JAR file support for REQUIRES_AUTOMATIC and REQUIRES_TRANSITIVE_AUTOMATIC
Path modulePath = base.resolve("modules");
resetDirectory(modulePath);
LintCategory category = test.category();
switch (category) {
case REQUIRES_AUTOMATIC:
case REQUIRES_TRANSITIVE_AUTOMATIC:
// Compile a simple automatic module (randomjar-1.0)
Path randomJarBase = base.resolve("randomjar");
tb.writeJavaFiles(getSourcesDir(randomJarBase), "package api; public class Api {}");
List<String> log = compile(randomJarBase, Task.Expect.SUCCESS, "-Werror");
if (!log.isEmpty()) {
throw new AssertionError(String.format(
"non-empty log output:%n %s", log.stream().collect(Collectors.joining("\n "))));
}
// JAR it up
Path automaticJar = modulePath.resolve("randomjar-1.0.jar");
new JarTask(tb, automaticJar)
.baseDir(getClassesDir(randomJarBase))
.files("api/Api.class")
.run();
break;
default:
modulePath = null;
break;
};
// Create a @SuppressWarnings annotation
String annotation = String.format("@SuppressWarnings(\"%s\")", category.option);
// See which annotation substitutions this test supports
boolean hasOuterAnnotation = test.sources().stream().anyMatch(source -> source.contains("@OUTER@"));
boolean hasInnerAnnotation = test.sources().stream().anyMatch(source -> source.contains("@INNER@"));
// Try all combinations of inner and outer @SuppressWarnings
boolean[] booleans = new boolean[] { false, true };
for (boolean outerAnnotation : booleans) { for (boolean innerAnnotation : booleans) {
// Skip this scenario if not supported by test case
if ((outerAnnotation && !hasOuterAnnotation) || (innerAnnotation && !hasInnerAnnotation))
continue;
// Insert or comment out the @SuppressWarnings annotations in the source templates
String[] sources = test.sources().stream()
.map(source -> source.replace("@OUTER@",
String.format("%s@SuppressWarnings(\"%s\")", outerAnnotation ? "" : "//", category.option)))
.map(source -> source.replace("@INNER@",
String.format("%s@SuppressWarnings(\"%s\")", innerAnnotation ? "" : "//", category.option)))
.toArray(String[]::new);
for (String source : sources) {
Path pkgRoot = getSourcesDir(base);
String moduleName = Optional.of("@MODULE@:(\\S+)")
.map(Pattern::compile)
.map(p -> p.matcher(source))
.filter(Matcher::find)
.map(m -> m.group(1))
.orElse(null);
if (moduleName != null) { // add an extra directory for module
if (!moduleNames.contains(moduleName))
throw new AssertionError(String.format("unknown module \"%s\" in %s", moduleName, category));
pkgRoot = pkgRoot.resolve(moduleName);
}
tb.writeJavaFiles(pkgRoot, source);
}
// Try all combinations of lint flags
for (boolean enableCategory : booleans) { // [-]category
for (boolean enableSuppression : booleans) { // [-]suppression
// Should we expect the warning to be emitted?
boolean expectCategoryWarning = category.annotationSuppression ?
enableCategory && !outerAnnotation && !innerAnnotation : enableCategory;
// Should we expect the SUPPRESSION warning to be emitted?
boolean expectSuppressionWarning = category.annotationSuppression ?
enableSuppression && outerAnnotation && innerAnnotation : // only if both, outer is redundant
enableSuppression && (outerAnnotation || innerAnnotation); // either one is always redundant
// Prepare command line flags
ArrayList<String> flags = new ArrayList<>();
if (modulePath != null) {
flags.add("--module-path");
flags.add(modulePath.toString());
}
flags.add("--release");
flags.add(Source.DEFAULT.name);
flags.addAll(test.compileFlags());
ArrayList<String> lints = new ArrayList<>();
lints.add(String.format("%s%s", enableCategory ? "" : "-", category.option));
if (enableSuppression)
lints.add(SUPPRESSION.option);
if (!lints.isEmpty())
flags.add("-Xlint:" + lints.stream().collect(Collectors.joining(",")));
// Test case description
String description = String.format("[%s] outer=%s inner=%s enable=%s flags=\"%s\"",
category, outerAnnotation, innerAnnotation, enableCategory,
flags.stream().collect(Collectors.joining(" ")));
// Only print log if test case fails
StringWriter buf = new StringWriter();
PrintWriter log = new PrintWriter(buf);
try {
// Logging
log.println(String.format(">>> Test START: %s", description));
Stream.of(sources).forEach(log::println);
log.println(String.format(">>> expectCategoryWarning=%s", expectCategoryWarning));
log.println(String.format(">>> expectSuppressionWarning=%s", expectSuppressionWarning));
// Compile sources and get log output
List<String> output = compile(base, Task.Expect.SUCCESS, flags.toArray(new String[0]));
// Scrub insignificant log output
output.removeIf(line -> line.matches("[0-9]+ (error|warning)s?"));
output.removeIf(line -> line.contains("compiler.err.warnings.and.werror"));
output.removeIf(line -> line.matches("- compiler\\.note\\..*")); // mandatory warning "recompile" etc.
// See which warnings appeared
boolean foundSuppressionWarning = output.removeIf(
line -> line.contains("compiler.warn.unnecessary.warning.suppression"));
boolean foundCategoryWarning = output.removeIf(line -> line.contains(test.warningKey()));
// Compare that vs. expectations
if (foundCategoryWarning != expectCategoryWarning) {
throw new AssertionError(String.format("%s: category warning: found=%s but expected=%s",
description, foundCategoryWarning, expectCategoryWarning));
}
if (foundSuppressionWarning != expectSuppressionWarning) {
throw new AssertionError(String.format("%s: \"%s\" warning: found=%s but expected=%s",
description, SUPPRESSION.option, foundSuppressionWarning, expectSuppressionWarning));
}
// There shouldn't be any other warnings
if (!output.isEmpty()) {
throw new AssertionError(String.format(
"%s: %d unexpected warning(s): %s", description, output.size(), output));
}
// Done
log.println(String.format("<<< Test PASSED: %s", description));
} catch (AssertionError e) {
log.println(String.format("<<< Test FAILED: %s", description));
log.flush();
out.print(buf);
throw e;
}
}
}
} }
}
// Test a @SuppressWarning annotation that suppresses nothing
@Test
public void testUselessAnnotation(LintCategory category) throws Exception {
compileAndExpectWarning(
"compiler.warn.unnecessary.warning.suppression",
String.format(
"""
@SuppressWarnings(\"%s\")
public class Test { }
""",
category.option),
String.format("-Xlint:%s", SUPPRESSION.option));
}
// Test the suppression of SUPPRESSION itself, which should always work,
// even when the same annotation uselessly suppresses some other category.
@Test
public void testSelfSuppression(LintCategory category) throws Exception {
// Test category and SUPPRESSION in the same annotation
compileAndExpectSuccess(
String.format(
"""
@SuppressWarnings({ \"%s\", \"%s\" })
public class Test {
}
""",
category.option, // this is actually a useless suppression
SUPPRESSION.option), // but this prevents us from reporting it
String.format("-Xlint:%s", SUPPRESSION.option));
// Test category and SUPPRESSION in nested annotations
compileAndExpectSuccess(
String.format(
"""
@SuppressWarnings(\"%s\") // suppress useless suppression warnings
public class Test {
@SuppressWarnings(\"%s\") // a useless suppression
public class Sub { }
}
""",
SUPPRESSION.option, // this prevents us from reporting the nested useless suppression
category.option), // this is a useless suppression
String.format("-Xlint:%s", SUPPRESSION.option));
}
// Test OVERLOADS which has tricky "either-or" suppression
@Test
public void testOverloads() throws Exception {
compileAndExpectSuccess(
"""
import java.util.function.*;
public class Super {
@SuppressWarnings("overloads")
public void foo(IntConsumer c) {
}
@SuppressWarnings("overloads")
public void foo(Consumer<Integer> c) {
}
}
""",
String.format("-Xlint:%s", OVERLOADS.option),
String.format("-Xlint:%s", SUPPRESSION.option));
}
// Test THIS_ESCAPE which has tricky control-flow based suppression
@Test
public void testThisEscape() throws Exception {
compileAndExpectSuccess(
"""
public class Test {
public Test() {
this(0);
}
@SuppressWarnings("this-escape")
private Test(int x) {
this.leak();
}
protected void leak() { }
}
""",
String.format("-Xlint:%s", THIS_ESCAPE.option),
String.format("-Xlint:%s", SUPPRESSION.option));
}
public void compileAndExpectWarning(String errorKey, String source, String... flags) throws Exception {
// Setup source & destination diretories
Path base = Paths.get("compileAndExpectWarning");
resetCompileDirectories(base);
// Write source file
tb.writeJavaFiles(getSourcesDir(base), source);
// Compile sources and verify we got the warning
List<String> log = compile(base, Task.Expect.FAIL, addWerror(flags));
if (log.stream().noneMatch(line -> line.contains(errorKey))) {
throw new AssertionError(String.format(
"did not find \"%s\" in log output:%n %s",
errorKey, log.stream().collect(Collectors.joining("\n "))));
}
}
public void compileAndExpectSuccess(String source, String... flags) throws Exception {
// Setup source & destination diretories
Path base = Paths.get("compileAndExpectSuccess");
resetCompileDirectories(base);
// Write source file
tb.writeJavaFiles(getSourcesDir(base), source);
// Compile sources and verify there is no log output
List<String> log = compile(base, Task.Expect.SUCCESS, addWerror(flags));
if (!log.isEmpty()) {
throw new AssertionError(String.format(
"non-empty log output:%n %s", log.stream().collect(Collectors.joining("\n "))));
}
}
private List<String> compile(Path base, Task.Expect expectation, String... flags) throws Exception {
ArrayList<String> options = new ArrayList<>();
options.add("-XDrawDiagnostics");
Stream.of(flags).forEach(options::add);
List<String> log;
try {
log = new JavacTask(tb, Mode.CMDLINE)
.options(options.toArray(new String[0]))
.files(tb.findJavaFiles(getSourcesDir(base)))
.outdir(getClassesDir(base))
.run(expectation)
.writeAll()
.getOutputLines(Task.OutputKind.DIRECT);
} catch (Task.TaskError e) {
throw new AssertionError(String.format(
"compile in %s failed: %s", getSourcesDir(base), e.getMessage()), e);
}
log.removeIf(line -> line.trim().isEmpty());
return log;
}
private Path getSourcesDir(Path base) {
return base.resolve("sources");
}
private Path getClassesDir(Path base) {
return base.resolve("classes");
}
private void resetCompileDirectories(Path base) throws IOException {
for (Path dir : List.of(getSourcesDir(base), getClassesDir(base)))
resetDirectory(dir);
}
private void resetDirectory(Path dir) throws IOException {
if (Files.exists(dir, LinkOption.NOFOLLOW_LINKS))
Files.walkFileTree(dir, new Deleter());
Files.createDirectories(dir);
}
private String[] addWerror(String[] flags) {
return Stream.concat(Stream.of(flags), Stream.of("-Werror")).toArray(String[]::new);
}
// SuppressTest
private record SuppressTest(
LintCategory category, // The Lint category being tested
String warningKey, // Expected warning message key in compiler.properties
List<String> compileFlags, // Any required compilation flags
List<String> sources // Source files with @MODULE@, @OUTER@ and @INNER@ placeholders
) {
SuppressTest(LintCategory category, String warningKey, String[] compileFlags, String... sources) {
this(category, warningKey, List.of(compileFlags != null ? compileFlags : new String[0]), List.of(sources));
}
}
// Deleter
private static class Deleter extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
}
}

View File

@ -1,7 +1,7 @@
/*
* @test /nodynamiccopyright/
* @bug 4986256
* @compile/ref=DepAnn.out -XDrawDiagnostics -Xlint:all,-dangling-doc-comments DepAnn.java
* @compile/ref=DepAnn.out -XDrawDiagnostics -Xlint:all,-dangling-doc-comments,-suppression DepAnn.java
*/
// control: this class should generate warnings

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -29,7 +29,12 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Utility class to manage and execute sub-tests within a test.
@ -74,28 +79,59 @@ public abstract class TestRunner {
}
/**
* Invoke all methods annotated with @Test.
* @param f a lambda expression to specify arguments for the test method
* Invoke each @Test method once using the parameters returned by the function.
*
* <p>
* If the function returns null for some method, that method is skipped.
*
* <p>
* If system property {@code test.query} is set, only that method is included.
*
* @param f function mapping method name to an array of method parameters
* @throws java.lang.Exception if any errors occur
*/
protected void runTests(Function<Method, Object[]> f) throws Exception {
runTestsMulti(f.andThen(Stream::<Object[]>of));
}
/**
* Invoke each @Test method once for each array of parameters returned by the iteration.
*
* <p>
* If the function returns null for some method, that method is skipped.
*
* @param f function mapping method name to an iteration of arrays of method parameters
* @throws java.lang.Exception if any errors occur
*/
protected void runTestsMulti(Function<Method, ? extends Stream<? extends Object[]>> f) throws Exception {
String testQuery = System.getProperty("test.query");
for (Method m : getClass().getDeclaredMethods()) {
Annotation a = m.getAnnotation(Test.class);
if (a != null) {
testName = m.getName();
if (testQuery == null || testQuery.equals(testName)) {
try {
testCount++;
out.println("test: " + testName);
m.invoke(this, f.apply(m));
} catch (InvocationTargetException e) {
errorCount++;
Throwable cause = e.getCause();
out.println("Exception running test " + testName + ": " + e.getCause());
cause.printStackTrace(out);
Iterator<? extends Object[]> iterator = Optional.of(m).map(f).map(Stream::iterator).orElse(null);
if (iterator == null)
return;
for (int testNum = 1; iterator.hasNext(); testNum++) {
Object[] params = iterator.next();
try {
testCount++;
out.println(String.format("test: %s#%d", testName, testNum));
m.invoke(this, params);
} catch (InvocationTargetException e) {
errorCount++;
Throwable cause = e.getCause();
String paramsDesc = Stream.of(params)
.map(String::valueOf)
.collect(Collectors.joining(", "));
out.println(String.format(
"Exception running test %s#%d(%s): %s",
testName, testNum, paramsDesc, e.getCause()));
cause.printStackTrace(out);
}
out.println();
}
out.println();
}
}
}