Skip to content

Commit

Permalink
Allow to silently ignore signatures if the class is not found (#164)
Browse files Browse the repository at this point in the history
Allow to silently ignore signatures if the class is not found (e.g., multi-module builds with common signatures). The old setting failOnUnresolvableSignatures was deprecated in favour of ignoreSignaturesOfMissingClasses. This closes #83
  • Loading branch information
uschindler authored Apr 23, 2020
1 parent 5dda144 commit fb656e1
Show file tree
Hide file tree
Showing 14 changed files with 279 additions and 33 deletions.
21 changes: 20 additions & 1 deletion src/main/docs/ant-task.html
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,15 @@ <h2>Parameters</h2>
<td>failOnUnresolvableSignatures</td>
<td><code>boolean</code></td>
<td><code>true</code></td>
<td>Fail the build if a signature is not resolving. If this parameter is set to <code>false</code>, then such signatures are silently ignored.</td>
<td>Fail the build if a signature is not resolving. If this parameter is set to
to false, then such signatures are ignored.<br>
When disabling this setting, the task still prints a warning to inform the user about
broken signatures. This cannot be disabled. There is a second setting
<code>ignoreSignaturesOfMissingClasses</code> that can be used to silently ignore
signatures that refer to methods or field in classes that are not on classpath,
e.g. This is useful in multi-module builds where a common set of signatures is used,
that are not part of every sub-modules dependencies.<br>
<strong>Deprecated.</strong> Use <code>ignoreSignaturesOfMissingClasses</code> instead.</td>
</tr>

<tr>
Expand All @@ -137,6 +145,17 @@ <h2>Parameters</h2>
<td>Ignore empty fileset/resource collection and print a warning instead.</td>
</tr>

<tr>
<td>ignoreSignaturesOfMissingClasses</td>
<td><code>boolean</code></td>
<td><code>false</code></td>
<td>If a class is missing while parsing signatures files, all methods and fields from this
class are silently ignored. This is useful in multi-module
projects where only some modules have the dependency to which the signature file(s) apply.
This settings prints no warning at all, so verify the signatures at least once with
full dependencies.</td>
</tr>

<tr>
<td>suppressAnnotation</td>
<td><code>class name</code></td>
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/de/thetaphi/forbiddenapis/Checker.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public static enum Option {
FAIL_ON_MISSING_CLASSES,
FAIL_ON_VIOLATION,
FAIL_ON_UNRESOLVABLE_SIGNATURES,
IGNORE_SIGNATURES_OF_MISSING_CLASSES,
DISABLE_CLASSLOADING_CACHE
}

Expand Down Expand Up @@ -341,6 +342,11 @@ public boolean hasNoSignatures() {
return forbiddenSignatures.hasNoSignatures();
}

/** Returns if no signatures files / inline signatures were parsed */
public boolean noSignaturesFilesParsed() {
return forbiddenSignatures.noSignaturesFilesParsed();
}

/** Parses and adds a class from the given stream to the list of classes to check. Closes the stream when parsed (on Exception, too)! Does not log anything. */
public void addClassToCheck(final InputStream in, String name) throws IOException {
final ClassReader reader;
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/de/thetaphi/forbiddenapis/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public interface Constants {

final Pattern JDK_SIG_PATTERN = Pattern.compile("(jdk\\-.*?\\-)(\\d+)(\\.\\d+)?(\\.\\d+)*");

final String DEPRECATED_WARN_FAIL_ON_UNRESOLVABLE_SIGNATURES =
"The setting 'failOnUnresolvableSignatures' was deprecated and will be removed in next version. Use 'ignoreSignaturesOfMissingClasses' instead.";

final Type DEPRECATED_TYPE = Type.getType(Deprecated.class);
final String DEPRECATED_DESCRIPTOR = DEPRECATED_TYPE.getDescriptor();

Expand Down
41 changes: 33 additions & 8 deletions src/main/java/de/thetaphi/forbiddenapis/Signatures.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public final class Signatures implements Constants {
private static final String BUNDLED_PREFIX = "@includeBundled ";
private static final String DEFAULT_MESSAGE_PREFIX = "@defaultMessage ";
private static final String IGNORE_UNRESOLVABLE_LINE = "@ignoreUnresolvable";
private static final String IGNORE_MISSING_CLASSES_LINE = "@ignoreMissingClasses";

private static enum UnresolvableReporting {
FAIL(true) {
Expand Down Expand Up @@ -78,7 +79,7 @@ private UnresolvableReporting(boolean reportClassNotFound) {

private final RelatedClassLookup lookup;
private final Logger logger;
private final boolean failOnUnresolvableSignatures;
private final boolean failOnUnresolvableSignatures, ignoreSignaturesOfMissingClasses;

/** Key is used to lookup forbidden signature in following formats. Keys are generated by the corresponding
* {@link #getKey(String)} (classes), {@link #getKey(String, Method)} (methods),
Expand All @@ -91,14 +92,18 @@ private UnresolvableReporting(boolean reportClassNotFound) {

/** if enabled, the bundled signature to enable heuristics for detection of non-portable runtime calls is used */
private boolean forbidNonPortableRuntime = false;

/** number of files that were interpreted as signatures file. If 0, no (bundled) signatures files were added at all */
private int numberOfFiles = 0;

public Signatures(Checker checker) {
this(checker, checker.logger, checker.options.contains(Option.FAIL_ON_UNRESOLVABLE_SIGNATURES));
this(checker, checker.logger, checker.options.contains(Option.IGNORE_SIGNATURES_OF_MISSING_CLASSES), checker.options.contains(Option.FAIL_ON_UNRESOLVABLE_SIGNATURES));
}

public Signatures(RelatedClassLookup lookup, Logger logger, boolean failOnUnresolvableSignatures) {
public Signatures(RelatedClassLookup lookup, Logger logger, boolean ignoreSignaturesOfMissingClasses, boolean failOnUnresolvableSignatures) {
this.lookup = lookup;
this.logger = logger;
this.ignoreSignaturesOfMissingClasses = ignoreSignaturesOfMissingClasses;
this.failOnUnresolvableSignatures = failOnUnresolvableSignatures;
}

Expand All @@ -115,7 +120,8 @@ static String getKey(String internalClassName, Method method) {
}

/** Adds the method signature to the list of disallowed methods. The Signature is checked against the given ClassLoader. */
private void addSignature(final String line, final String defaultMessage, final UnresolvableReporting report, final Set<String> missingClasses) throws ParseException,IOException {
private void addSignature(final String line, final String defaultMessage, final UnresolvableReporting report,
final boolean localIgnoreMissingClasses, final Set<String> missingClasses) throws ParseException,IOException {
final String clazz, field, signature;
String message = null;
final Method method;
Expand Down Expand Up @@ -171,6 +177,9 @@ private void addSignature(final String line, final String defaultMessage, final
try {
c = lookup.getClassFromClassLoader(clazz);
} catch (ClassNotFoundException cnfe) {
if (this.ignoreSignaturesOfMissingClasses || localIgnoreMissingClasses) {
return;
}
if (report.reportClassNotFound) {
report.parseFailed(logger, String.format(Locale.ENGLISH, "Class '%s' not found on classpath", cnfe.getMessage()), signature);
} else {
Expand Down Expand Up @@ -235,6 +244,7 @@ private void addBundledSignatures(String name, String jdkTargetVersion, boolean
}
if (BS_JDK_NONPORTABLE.equals(name)) {
if (logging) logger.info("Reading bundled API signatures: " + name);
numberOfFiles++;
forbidNonPortableRuntime = true;
return;
}
Expand All @@ -254,14 +264,16 @@ private void addBundledSignatures(String name, String jdkTargetVersion, boolean
parseSignaturesStream(in, true, missingClasses);
}

private void parseSignaturesStream(InputStream in, boolean allowBundled, Set<String> missingClasses) throws IOException,ParseException {
parseSignaturesFile(new InputStreamReader(in, StandardCharsets.UTF_8), allowBundled, missingClasses);
private void parseSignaturesStream(InputStream in, boolean isBundled, Set<String> missingClasses) throws IOException,ParseException {
parseSignaturesFile(new InputStreamReader(in, StandardCharsets.UTF_8), isBundled, missingClasses);
}

private void parseSignaturesFile(Reader reader, boolean isBundled, Set<String> missingClasses) throws IOException,ParseException {
numberOfFiles++;
try (final BufferedReader r = new BufferedReader(reader)) {
String line, defaultMessage = null;
UnresolvableReporting reporter = failOnUnresolvableSignatures ? UnresolvableReporting.FAIL : UnresolvableReporting.WARNING;
boolean localIgnoreMissingClasses = false;
while ((line = r.readLine()) != null) {
line = line.trim();
if (line.length() == 0 || line.startsWith("#"))
Expand All @@ -274,12 +286,20 @@ private void parseSignaturesFile(Reader reader, boolean isBundled, Set<String> m
defaultMessage = line.substring(DEFAULT_MESSAGE_PREFIX.length()).trim();
if (defaultMessage.length() == 0) defaultMessage = null;
} else if (line.equals(IGNORE_UNRESOLVABLE_LINE)) {
reporter = isBundled ? UnresolvableReporting.SILENT : UnresolvableReporting.WARNING;
if (isBundled) {
reporter = UnresolvableReporting.SILENT;
} else {
logger.warn(String.format(Locale.ENGLISH, "'%s' inside signatures files is deprecated, prefer using '%s' to ignore signatures where the class is missing.",
IGNORE_UNRESOLVABLE_LINE, IGNORE_MISSING_CLASSES_LINE));
reporter = UnresolvableReporting.WARNING;
}
} else if (line.equals(IGNORE_MISSING_CLASSES_LINE)) {
localIgnoreMissingClasses = true;
} else {
throw new ParseException("Invalid line in signature file: " + line);
}
} else {
addSignature(line, defaultMessage, reporter, missingClasses);
addSignature(line, defaultMessage, reporter, localIgnoreMissingClasses, missingClasses);
}
}
}
Expand Down Expand Up @@ -315,6 +335,11 @@ public boolean hasNoSignatures() {
(forbidNonPortableRuntime ? 1 : 0);
}

/** Returns if no signatures files / inline signatures were parsed */
public boolean noSignaturesFilesParsed() {
return numberOfFiles == 0;
}

/** Returns if bundled signature to enable heuristics for detection of non-portable runtime calls is used */
public boolean isNonPortableRuntimeForbidden() {
return this.forbidNonPortableRuntime;
Expand Down
40 changes: 36 additions & 4 deletions src/main/java/de/thetaphi/forbiddenapis/ant/AntTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public class AntTask extends Task implements Constants {
private boolean restrictClassFilename = true;
private boolean failOnMissingClasses = true;
private boolean failOnUnresolvableSignatures = true;
private boolean ignoreSignaturesOfMissingClasses = false;
private boolean failOnViolation = true;
private boolean ignoreEmptyFileset = false;
private String targetVersion = null;
Expand Down Expand Up @@ -108,7 +109,12 @@ public void info(String msg) {
final EnumSet<Checker.Option> options = EnumSet.noneOf(Checker.Option.class);
if (failOnMissingClasses) options.add(FAIL_ON_MISSING_CLASSES);
if (failOnViolation) options.add(FAIL_ON_VIOLATION);
if (failOnUnresolvableSignatures) options.add(FAIL_ON_UNRESOLVABLE_SIGNATURES);
if (failOnUnresolvableSignatures) {
options.add(FAIL_ON_UNRESOLVABLE_SIGNATURES);
} else {
log.warn(DEPRECATED_WARN_FAIL_ON_UNRESOLVABLE_SIGNATURES);
}
if (ignoreSignaturesOfMissingClasses) options.add(IGNORE_SIGNATURES_OF_MISSING_CLASSES);
if (disableClassloadingCache) options.add(DISABLE_CLASSLOADING_CACHE);
final Checker checker = new Checker(log, loader, options);

Expand Down Expand Up @@ -169,7 +175,12 @@ public void info(String msg) {
}

if (checker.hasNoSignatures()) {
throw new BuildException("No API signatures found; use signaturesFile=, <signatures*/>, <bundledSignatures/> or inner text to define those!");
if (checker.noSignaturesFilesParsed()) {
throw new BuildException("No signatures were added to task; use signaturesFile=, <signatures*/>, <bundledSignatures/> or inner text to define those!");
} else {
log.info("Skipping execution because no API signatures are available.");
return;
}
}

log.info("Loading classes to check...");
Expand Down Expand Up @@ -325,13 +336,34 @@ public void setFailOnMissingClasses(boolean failOnMissingClasses) {

/**
* Fail the build if a signature is not resolving. If this parameter is set to
* to false, then such signatures are silently ignored.
* Defaults to {@code true}.
* to false, then such signatures are ignored. Defaults to {@code true}.
* <p>When disabling this setting, the task still prints a warning to inform the user about
* broken signatures. This cannot be disabled. There is a second setting
* {@link #setIgnoreMissingSignaturesClasses()} that can be used to silently ignore
* signatures that refer to methods or field in classes that are not on classpath,
* e.g. This is useful in multi-module builds where a common set of signatures is used,
* that are not part of every sub-modules dependencies.
* @see #setIgnoreMissingSignaturesClasses()
* @deprecated Use {@link #setIgnoreSignaturesOfMissingClasses(boolean)} instead.
*/
@Deprecated
public void setFailOnUnresolvableSignatures(boolean failOnUnresolvableSignatures) {
this.failOnUnresolvableSignatures = failOnUnresolvableSignatures;
}

/**
* If a class is missing while parsing signatures files, all methods and fields from this
* class are silently ignored. This is useful in multi-module
* projects where only some modules have the dependency to which the signature file(s) apply.
* This settings prints no warning at all, so verify the signatures at least once with
* full dependencies.
* Defaults to {@code false}.
* @since 3.0
*/
public void setIgnoreSignaturesOfMissingClasses(boolean ignoreSignaturesOfMissingClasses) {
this.ignoreSignaturesOfMissingClasses = ignoreSignaturesOfMissingClasses;
}

/** Automatically restrict resource names included to files with a name ending in '.class'.
* This makes filesets easier, as the includes="**&#47;*.class" is not needed.
* Defaults to {@code true}.
Expand Down
28 changes: 21 additions & 7 deletions src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
public final class CliMain implements Constants {

private final Option classpathOpt, dirOpt, includesOpt, excludesOpt, signaturesfileOpt, bundledsignaturesOpt, suppressannotationsOpt,
allowmissingclassesOpt, allowunresolvablesignaturesOpt, versionOpt, helpOpt;
allowmissingclassesOpt, ignoresignaturesofmissingclassesOpt, allowunresolvablesignaturesOpt, versionOpt, helpOpt;
private final CommandLine cmd;

private static final Logger LOG = StdIoLogger.INSTANCE;
Expand Down Expand Up @@ -129,8 +129,12 @@ public CliMain(String... args) throws ExitException {
.desc("don't fail if a referenced class is missing on classpath")
.longOpt("allowmissingclasses")
.build());
options.addOption(ignoresignaturesofmissingclassesOpt = Option.builder()
.desc("if a class is missing while parsing signatures files, all methods and fields from this class are silently ignored")
.longOpt("ignoresignaturesofmissingclasses")
.build());
options.addOption(allowunresolvablesignaturesOpt = Option.builder()
.desc("don't fail if a signature is not resolving")
.desc("DEPRECATED: don't fail if a signature is not resolving")
.longOpt("allowunresolvablesignatures")
.build());

Expand Down Expand Up @@ -212,7 +216,12 @@ public void run() throws ExitException {
try (final URLClassLoader loader = URLClassLoader.newInstance(urls, ClassLoader.getSystemClassLoader())) {
final EnumSet<Checker.Option> options = EnumSet.of(FAIL_ON_VIOLATION);
if (!cmd.hasOption(allowmissingclassesOpt.getLongOpt())) options.add(FAIL_ON_MISSING_CLASSES);
if (!cmd.hasOption(allowunresolvablesignaturesOpt.getLongOpt())) options.add(FAIL_ON_UNRESOLVABLE_SIGNATURES);
if (cmd.hasOption(allowunresolvablesignaturesOpt.getLongOpt())) {
LOG.warn(DEPRECATED_WARN_FAIL_ON_UNRESOLVABLE_SIGNATURES);
} else {
options.add(FAIL_ON_UNRESOLVABLE_SIGNATURES);
}
if (cmd.hasOption(ignoresignaturesofmissingclassesOpt.getLongOpt())) options.add(IGNORE_SIGNATURES_OF_MISSING_CLASSES);
final Checker checker = new Checker(LOG, loader, options);

if (!checker.isSupportedJDK) {
Expand Down Expand Up @@ -267,10 +276,15 @@ public void run() throws ExitException {
}

if (checker.hasNoSignatures()) {
throw new ExitException(EXIT_ERR_CMDLINE, String.format(Locale.ENGLISH,
"No API signatures found; use parameters '--%s' and/or '--%s' to specify those!",
bundledsignaturesOpt.getLongOpt(), signaturesfileOpt.getLongOpt()
));
if (checker.noSignaturesFilesParsed()) {
throw new ExitException(EXIT_ERR_CMDLINE, String.format(Locale.ENGLISH,
"No API signatures given as parameters; use '--%s' and/or '--%s' to specify those!",
bundledsignaturesOpt.getLongOpt(), signaturesfileOpt.getLongOpt()
));
} else {
LOG.info("Skipping execution because no API signatures are available.");
return;
}
}

try {
Expand Down
Loading

0 comments on commit fb656e1

Please sign in to comment.