diff --git a/src/main/docs/ant-task.html b/src/main/docs/ant-task.html index 9a7c629..bd78c06 100644 --- a/src/main/docs/ant-task.html +++ b/src/main/docs/ant-task.html @@ -73,6 +73,20 @@

Parameters

Name of a built-in signatures file. + + signaturesWithSeveritySuppress + String + + A forbidden API signature for which violations should not be reported at all (i.e. neither fail the build nor appear in the logs). This takes precedence overfailOnViolation and signaturesWithSeverityWarn. + + + + signaturesWithSeverityWarn + String + + A forbidden API signature for which violations should be reported as warnings (i.e. not fail the build). This takes precedence overfailOnViolation. + + classpath Path @@ -184,7 +198,7 @@

Parameters specified as nested elements

This task supports all Ant resource types (fileset, filelist, file, tarfileset, zipfileset,...) -and uses all class files from them. It automatically adds an implcit filter to file names ending in '.class', +and uses all class files from them. It automatically adds an implicit filter to file names ending in '.class', so you don't need to add this as include attribute to those collections.

You can also pass one or multiple classpath elements to form a classpath. Ideally use the same configuration like the javac task.

diff --git a/src/main/java/de/thetaphi/forbiddenapis/Checker.java b/src/main/java/de/thetaphi/forbiddenapis/Checker.java index 7efbf5b..8998405 100644 --- a/src/main/java/de/thetaphi/forbiddenapis/Checker.java +++ b/src/main/java/de/thetaphi/forbiddenapis/Checker.java @@ -30,6 +30,7 @@ import java.net.URL; import java.net.URLConnection; import java.util.Arrays; +import java.util.Collection; import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedHashSet; @@ -45,6 +46,8 @@ import org.objectweb.asm.ClassReader; import org.objectweb.asm.Type; +import de.thetaphi.forbiddenapis.Checker.ViolationSeverity; + /** * Forbidden APIs checker class. */ @@ -58,6 +61,10 @@ public static enum Option { DISABLE_CLASSLOADING_CACHE } + public enum ViolationSeverity { + ERROR, WARNING, INFO, DEBUG, SUPPRESS + } + public final boolean isSupportedJDK; private final long start; @@ -360,6 +367,11 @@ public boolean noSignaturesFilesParsed() { return forbiddenSignatures.noSignaturesFilesParsed(); } + /** Adjusts the severity of a specific signature. */ + public void setSignaturesSeverity(Collection signatures, ViolationSeverity severity) throws ParseException, IOException { + forbiddenSignatures.setSignaturesSeverity(signatures, severity); + } + /** 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; @@ -417,7 +429,7 @@ public void addSuppressAnnotation(String annoName) { /** Parses a class and checks for valid method invocations */ private int checkClass(ClassMetadata c, Pattern suppressAnnotationsPattern) throws ForbiddenApiException { final String className = c.getBinaryClassName(); - final ClassScanner scanner = new ClassScanner(c, this, forbiddenSignatures, suppressAnnotationsPattern); + final ClassScanner scanner = new ClassScanner(c, this, forbiddenSignatures, suppressAnnotationsPattern, options.contains(Option.FAIL_ON_VIOLATION)); try { c.getReader().accept(scanner, ClassReader.SKIP_FRAMES); } catch (RelatedClassLoadingException rcle) { @@ -452,12 +464,31 @@ private int checkClass(ClassMetadata c, Pattern suppressAnnotationsPattern) thro } final List violations = scanner.getSortedViolations(); final Pattern splitter = Pattern.compile(Pattern.quote(ForbiddenViolation.SEPARATOR)); + int numErrors = 0; for (final ForbiddenViolation v : violations) { + if (v.severity == ViolationSeverity.ERROR) { + numErrors++; + } for (final String line : splitter.split(v.format(className, scanner.getSourceFile()))) { - logger.error(line); + switch (v.severity) { + case DEBUG: + logger.debug(line); + break; + case INFO: + logger.info(line); + break; + case WARNING: + logger.warn(line); + break; + case ERROR: + logger.error(line); + break; + default: + break; + } } } - return violations.size(); + return numErrors; } public void run() throws ForbiddenApiException { @@ -483,5 +514,4 @@ public void run() throws ForbiddenApiException { logger.info(message); } } - } diff --git a/src/main/java/de/thetaphi/forbiddenapis/ClassScanner.java b/src/main/java/de/thetaphi/forbiddenapis/ClassScanner.java index 149136a..050c1bf 100644 --- a/src/main/java/de/thetaphi/forbiddenapis/ClassScanner.java +++ b/src/main/java/de/thetaphi/forbiddenapis/ClassScanner.java @@ -41,6 +41,9 @@ import org.objectweb.asm.TypePath; import org.objectweb.asm.commons.Method; +import de.thetaphi.forbiddenapis.Checker.ViolationSeverity; +import de.thetaphi.forbiddenapis.Signatures.ViolationResult; + public final class ClassScanner extends ClassVisitor implements Constants { private final boolean forbidNonPortableRuntime; final ClassMetadata metadata; @@ -63,14 +66,16 @@ public final class ClassScanner extends ClassVisitor implements Constants { // all groups that were disabled due to suppressing annotation final BitSet suppressedGroups = new BitSet(); boolean classSuppressed = false; + private final boolean failOnViolation; - public ClassScanner(ClassMetadata metadata, RelatedClassLookup lookup, Signatures forbiddenSignatures, final Pattern suppressAnnotations) { + public ClassScanner(ClassMetadata metadata, RelatedClassLookup lookup, Signatures forbiddenSignatures, final Pattern suppressAnnotations, boolean failOnViolation) { super(Opcodes.ASM9); this.metadata = metadata; this.lookup = lookup; this.forbiddenSignatures = forbiddenSignatures; this.suppressAnnotations = suppressAnnotations; this.forbidNonPortableRuntime = forbiddenSignatures.isNonPortableRuntimeForbidden(); + this.failOnViolation = failOnViolation; } private void checkDone() { @@ -87,14 +92,14 @@ public String getSourceFile() { return source; } - String checkClassUse(Type type, String what, boolean isAnnotation, String origInternalName) { + ViolationResult checkClassUse(Type type, String what, boolean isAnnotation, String origInternalName) { while (type.getSort() == Type.ARRAY) { type = type.getElementType(); // unwrap array } if (type.getSort() != Type.OBJECT) { return null; // we don't know this type, just pass! } - final String violation = forbiddenSignatures.checkType(type, what); + final ViolationResult violation = forbiddenSignatures.checkType(type, what); if (violation != null) { return violation; } @@ -103,10 +108,9 @@ String checkClassUse(Type type, String what, boolean isAnnotation, String origIn final String binaryClassName = type.getClassName(); final ClassMetadata c = lookup.lookupRelatedClass(type.getInternalName(), origInternalName); if (c != null && c.isNonPortableRuntime) { - return String.format(Locale.ENGLISH, + return new ViolationResult(String.format(Locale.ENGLISH, "Forbidden %s use: %s [non-portable or internal runtime class]", - what, binaryClassName - ); + what, binaryClassName), failOnViolation ? ViolationSeverity.ERROR : ViolationSeverity.WARNING); } } catch (RelatedClassLoadingException e) { // only throw exception if it is not an annotation @@ -115,20 +119,20 @@ String checkClassUse(Type type, String what, boolean isAnnotation, String origIn return null; } - String checkClassUse(String internalName, String what, String origInternalName) { + ViolationResult checkClassUse(String internalName, String what, String origInternalName) { return checkClassUse(Type.getObjectType(internalName), what, false, origInternalName); } // TODO: @FunctionalInterface from Java 8 on static interface AncestorVisitor { - final String STOP = new String("STOP"); + final ViolationResult STOP = new ViolationResult("STOP", null); - String visit(ClassMetadata c, String origName, boolean isInterfaceOfAncestor, boolean previousInRuntime); + ViolationResult visit(ClassMetadata c, String origName, boolean isInterfaceOfAncestor, boolean previousInRuntime); } - String visitAncestors(ClassMetadata cls, AncestorVisitor visitor, boolean visitSelf, boolean visitInterfacesFirst) { + ViolationResult visitAncestors(ClassMetadata cls, AncestorVisitor visitor, boolean visitSelf, boolean visitInterfacesFirst) { if (visitSelf) { - final String result = visitor.visit(cls, cls.className, cls.isInterface, cls.isRuntimeClass); + final ViolationResult result = visitor.visit(cls, cls.className, cls.isInterface, cls.isRuntimeClass); if (result == AncestorVisitor.STOP) { return null; } @@ -139,11 +143,11 @@ String visitAncestors(ClassMetadata cls, AncestorVisitor visitor, boolean visitS return visitAncestorsRecursive(cls, cls.className, visitor, cls.isRuntimeClass, visitInterfacesFirst); } - private String visitSuperclassRecursive(ClassMetadata cls, String origName, AncestorVisitor visitor, boolean previousInRuntime, boolean visitInterfacesFirst) { + private ViolationResult visitSuperclassRecursive(ClassMetadata cls, String origName, AncestorVisitor visitor, boolean previousInRuntime, boolean visitInterfacesFirst) { if (cls.superName != null) { final ClassMetadata c = lookup.lookupRelatedClass(cls.superName, origName); if (c != null) { - String result = visitor.visit(c, origName, false, previousInRuntime); + ViolationResult result = visitor.visit(c, origName, false, previousInRuntime); if (result != AncestorVisitor.STOP) { if (result != null) { return result; @@ -158,12 +162,12 @@ private String visitSuperclassRecursive(ClassMetadata cls, String origName, Ance return null; } - private String visitInterfacesRecursive(ClassMetadata cls, String origName, AncestorVisitor visitor, boolean previousInRuntime, boolean visitInterfacesFirst) { + private ViolationResult visitInterfacesRecursive(ClassMetadata cls, String origName, AncestorVisitor visitor, boolean previousInRuntime, boolean visitInterfacesFirst) { if (cls.interfaces != null) { for (String intf : cls.interfaces) { final ClassMetadata c = lookup.lookupRelatedClass(intf, origName); if (c == null) continue; - String result = visitor.visit(c, origName, true, previousInRuntime); + ViolationResult result = visitor.visit(c, origName, true, previousInRuntime); if (result != AncestorVisitor.STOP) { if (result != null) { return result; @@ -178,8 +182,8 @@ private String visitInterfacesRecursive(ClassMetadata cls, String origName, Ance return null; } - private String visitAncestorsRecursive(ClassMetadata cls, String origName, AncestorVisitor visitor, boolean previousInRuntime, boolean visitInterfacesFirst) { - String result; + private ViolationResult visitAncestorsRecursive(ClassMetadata cls, String origName, AncestorVisitor visitor, boolean previousInRuntime, boolean visitInterfacesFirst) { + ViolationResult result; if (visitInterfacesFirst) { result = visitInterfacesRecursive(cls, origName, visitor, previousInRuntime, visitInterfacesFirst); if (result != null) { @@ -202,7 +206,7 @@ private String visitAncestorsRecursive(ClassMetadata cls, String origName, Ances // TODO: convert to lambda method with method reference private final AncestorVisitor classRelationAncestorVisitor = new AncestorVisitor() { @Override - public String visit(ClassMetadata c, String origName, boolean isInterfaceOfAncestor, boolean previousInRuntime) { + public ViolationResult visit(ClassMetadata c, String origName, boolean isInterfaceOfAncestor, boolean previousInRuntime) { if (previousInRuntime && c.isNonPortableRuntime) { return null; // something inside the JVM is extending internal class/interface } @@ -210,9 +214,9 @@ public String visit(ClassMetadata c, String origName, boolean isInterfaceOfAnces } }; - String checkType(Type type) { + ViolationResult checkType(Type type) { while (type != null) { - String violation; + ViolationResult violation; switch (type.getSort()) { case Type.OBJECT: final String internalName = type.getInternalName(); @@ -226,7 +230,7 @@ String checkType(Type type) { type = type.getElementType(); break; case Type.METHOD: - final ArrayList violations = new ArrayList<>(); + final ArrayList violations = new ArrayList<>(); violation = checkType(type.getReturnType()); if (violation != null) { violations.add(violation); @@ -244,12 +248,17 @@ String checkType(Type type) { } else { final StringBuilder sb = new StringBuilder(); boolean nl = false; - for (final String v : violations) { + ViolationSeverity severity = null; + for (final ViolationResult v : violations) { if (nl) sb.append(ForbiddenViolation.SEPARATOR); - sb.append(v); + sb.append(v.message); nl = true; + // use the highest severity reported on this method + if (severity == null || v.severity.ordinal() > severity.ordinal()) { + severity = v.severity; + } } - return sb.toString(); + return new ViolationResult(sb.toString(), severity); } default: return null; @@ -258,11 +267,11 @@ String checkType(Type type) { return null; } - String checkDescriptor(String desc) { + ViolationResult checkDescriptor(String desc) { return checkType(Type.getType(desc)); } - String checkAnnotationDescriptor(Type type, boolean visible) { + ViolationResult checkAnnotationDescriptor(Type type, boolean visible) { // for annotations, we don't need to look into super-classes, interfaces,... return checkClassUse(type, "annotation", true, type.getInternalName()); } @@ -273,9 +282,9 @@ void maybeSuppressCurrentGroup(Type annotation) { } } - private void reportClassViolation(String violation, String where) { + private void reportClassViolation(ViolationResult violation, String where) { if (violation != null) { - violations.add(new ForbiddenViolation(currentGroupId, violation, where, -1)); + violations.add(new ForbiddenViolation(currentGroupId, violation.message, where, -1, violation.severity)); } } @@ -352,9 +361,9 @@ public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, Str return null; } - private void reportFieldViolation(String violation, String where) { - if (violation != null) { - violations.add(new ForbiddenViolation(currentGroupId, violation, String.format(Locale.ENGLISH, "%s of '%s'", where, name), -1)); + private void reportFieldViolation(ViolationResult violationResult, String where) { + if (violationResult != null) { + violations.add(new ForbiddenViolation(currentGroupId, violationResult.message, String.format(Locale.ENGLISH, "%s of '%s'", where, name), -1, violationResult.severity)); } } }; @@ -382,12 +391,12 @@ public MethodVisitor visitMethod(final int access, final String name, final Stri } } - private String checkMethodAccess(String owner, final Method method, final boolean callIsVirtual) { + private ViolationResult checkMethodAccess(String owner, final Method method, final boolean callIsVirtual) { if (CLASS_CONSTRUCTOR_METHOD_NAME.equals(method.getName())) { // we don't check for violations on class constructors return null; } - String violation = checkClassUse(owner, "class/interface", owner); + ViolationResult violation = checkClassUse(owner, "class/interface", owner); if (violation != null) { return violation; } @@ -405,7 +414,7 @@ private String checkMethodAccess(String owner, final Method method, final boolea } return visitAncestors(c, new AncestorVisitor() { @Override - public String visit(ClassMetadata c, String origName, boolean isInterfaceOfAncestor, boolean previousInRuntime) { + public ViolationResult visit(ClassMetadata c, String origName, boolean isInterfaceOfAncestor, boolean previousInRuntime) { final Method lookupMethod; if (c.signaturePolymorphicMethods.contains(method.getName())) { // convert the invoked descriptor to a signature polymorphic one for the lookup @@ -417,11 +426,11 @@ public String visit(ClassMetadata c, String origName, boolean isInterfaceOfAnces return null; } // is we have a virtual call, look into superclasses, otherwise stop: - final String notFoundRet = callIsVirtual ? null : AncestorVisitor.STOP; + final ViolationResult notFoundRet = callIsVirtual ? null : AncestorVisitor.STOP; if (previousInRuntime && c.isNonPortableRuntime) { return notFoundRet; // something inside the JVM is extending internal class/interface } - String violation = forbiddenSignatures.checkMethod(c.className, lookupMethod); + ViolationResult violation = forbiddenSignatures.checkMethod(c.className, lookupMethod); if (violation != null) { return violation; } @@ -437,8 +446,8 @@ public String visit(ClassMetadata c, String origName, boolean isInterfaceOfAnces }, true, false /* JVM spec says: interfaces after superclasses */); } - private String checkFieldAccess(String owner, final String field) { - String violation = checkClassUse(owner, "class/interface", owner); + private ViolationResult checkFieldAccess(String owner, final String field) { + ViolationResult violation = checkClassUse(owner, "class/interface", owner); if (violation != null) { return violation; } @@ -453,7 +462,7 @@ private String checkFieldAccess(String owner, final String field) { } return visitAncestors(c, new AncestorVisitor() { @Override - public String visit(ClassMetadata c, String origName, boolean isInterfaceOfAncestor, boolean previousInRuntime) { + public ViolationResult visit(ClassMetadata c, String origName, boolean isInterfaceOfAncestor, boolean previousInRuntime) { if (!c.fields.contains(field)) { return null; } @@ -461,7 +470,7 @@ public String visit(ClassMetadata c, String origName, boolean isInterfaceOfAnces if (previousInRuntime && c.isNonPortableRuntime) { return STOP; // something inside the JVM is extending internal class/interface } - String violation = forbiddenSignatures.checkField(c.className, field); + ViolationResult violation = forbiddenSignatures.checkField(c.className, field); if (violation != null) { return violation; } @@ -478,7 +487,7 @@ public String visit(ClassMetadata c, String origName, boolean isInterfaceOfAnces }, true, true /* JVM spec says: superclasses after interfaces */); } - private String checkHandle(Handle handle, boolean checkLambdaHandle) { + private ViolationResult checkHandle(Handle handle, boolean checkLambdaHandle) { switch (handle.getTag()) { case Opcodes.H_GETFIELD: case Opcodes.H_PUTFIELD: @@ -503,7 +512,7 @@ private String checkHandle(Handle handle, boolean checkLambdaHandle) { return null; } - private String checkConstant(Object cst, boolean checkLambdaHandle) { + private ViolationResult checkConstant(Object cst, boolean checkLambdaHandle) { if (cst instanceof Type) { return checkType((Type) cst); } else if (cst instanceof Handle) { @@ -604,9 +613,9 @@ private String getHumanReadableMethodSignature() { return sb.toString(); } - private void reportMethodViolation(String violation, String where) { + private void reportMethodViolation(ViolationResult violation, String where) { if (violation != null) { - violations.add(new ForbiddenViolation(currentGroupId, myself, violation, String.format(Locale.ENGLISH, "%s of '%s'", where, getHumanReadableMethodSignature()), lineNo)); + violations.add(new ForbiddenViolation(currentGroupId, myself, violation.message, String.format(Locale.ENGLISH, "%s of '%s'", where, getHumanReadableMethodSignature()), lineNo, violation.severity)); } } @@ -642,9 +651,9 @@ public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, Str return null; } - private void reportRecordComponentViolation(String violation, String where) { - if (violation != null) { - violations.add(new ForbiddenViolation(currentGroupId, violation, String.format(Locale.ENGLISH, "%s of '%s'", where, name), -1)); + private void reportRecordComponentViolation(ViolationResult violationResult, String where) { + if (violationResult != null) { + violations.add(new ForbiddenViolation(currentGroupId, violationResult.message, String.format(Locale.ENGLISH, "%s of '%s'", where, name), -1, violationResult.severity)); } } }; diff --git a/src/main/java/de/thetaphi/forbiddenapis/ForbiddenViolation.java b/src/main/java/de/thetaphi/forbiddenapis/ForbiddenViolation.java index 5640634..2ed43d5 100644 --- a/src/main/java/de/thetaphi/forbiddenapis/ForbiddenViolation.java +++ b/src/main/java/de/thetaphi/forbiddenapis/ForbiddenViolation.java @@ -21,6 +21,8 @@ import org.objectweb.asm.commons.Method; +import de.thetaphi.forbiddenapis.Checker.ViolationSeverity; + public final class ForbiddenViolation implements Comparable { /** Separator used to allow multiple description lines per violation. */ @@ -31,17 +33,19 @@ public final class ForbiddenViolation implements Comparable public final String description; public final String locationInfo; public final int lineNo; + public final ViolationSeverity severity; - ForbiddenViolation(int groupId, String description, String locationInfo, int lineNo) { - this(groupId, null, description, locationInfo, lineNo); + ForbiddenViolation(int groupId, String description, String locationInfo, int lineNo, ViolationSeverity severity) { + this(groupId, null, description, locationInfo, lineNo, severity); } - ForbiddenViolation(int groupId, Method targetMethod, String description, String locationInfo, int lineNo) { + ForbiddenViolation(int groupId, Method targetMethod, String description, String locationInfo, int lineNo, ViolationSeverity severity) { this.groupId = groupId; this.targetMethod = targetMethod; this.description = description; this.locationInfo = locationInfo; this.lineNo = lineNo; + this.severity = severity; } public void setGroupId(int groupId) { diff --git a/src/main/java/de/thetaphi/forbiddenapis/Signatures.java b/src/main/java/de/thetaphi/forbiddenapis/Signatures.java index 9e9a746..c862801 100644 --- a/src/main/java/de/thetaphi/forbiddenapis/Signatures.java +++ b/src/main/java/de/thetaphi/forbiddenapis/Signatures.java @@ -26,8 +26,12 @@ import java.io.Reader; import java.io.StringReader; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Locale; import java.util.Map; @@ -40,6 +44,7 @@ import org.objectweb.asm.commons.Method; import de.thetaphi.forbiddenapis.Checker.Option; +import de.thetaphi.forbiddenapis.Checker.ViolationSeverity; /** Utility class that is used to get an overview of all fields and implemented * methods of a class. It make the signatures available as Sets. */ @@ -94,21 +99,32 @@ private UnresolvableReporting(boolean reportClassNotFound) { /** set of patterns of forbidden classes */ final Set classPatterns = new LinkedHashSet<>(); + /** 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), + * {@link #getKey(String, String)} (fields) call. + */ + final Map severityPerSignature = new HashMap<>(); + final Map severityPerClassPattern = new HashMap<>(); + /** 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; + /** determines default severity for violations if no severity on signature level is overridden. true = ERROR, false = WARNING */ + private boolean failOnViolation; + public Signatures(Checker checker) { - this(checker, checker.logger, checker.options.contains(Option.IGNORE_SIGNATURES_OF_MISSING_CLASSES), 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), checker.options.contains(Option.FAIL_ON_VIOLATION)); } - public Signatures(RelatedClassLookup lookup, Logger logger, boolean ignoreSignaturesOfMissingClasses, boolean failOnUnresolvableSignatures) { + public Signatures(RelatedClassLookup lookup, Logger logger, boolean ignoreSignaturesOfMissingClasses, boolean failOnUnresolvableSignatures, boolean failOnViolation) { this.lookup = lookup; this.logger = logger; this.ignoreSignaturesOfMissingClasses = ignoreSignaturesOfMissingClasses; this.failOnUnresolvableSignatures = failOnUnresolvableSignatures; + this.failOnViolation = failOnViolation; } static String getKey(String internalClassName) { @@ -126,9 +142,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 boolean localIgnoreMissingClasses, final Set missingClasses) throws ParseException,IOException { - final String clazz, field, signature; String message = null; - final Method method; + String signature; int p = line.indexOf('@'); if (p >= 0) { signature = line.substring(0, p).trim(); @@ -140,6 +155,30 @@ private void addSignature(final String line, final String defaultMessage, final if (line.isEmpty()) { throw new ParseException("Empty signature"); } + if (message != null && message.isEmpty()) { + message = null; + } + // create printout message: + final String printout = (message != null) ? (signature + " [" + message + "]") : signature; + Collection keys = getKeys(report, localIgnoreMissingClasses, missingClasses, signature); + if (keys != null) { + for (String key : keys) { + if (key.startsWith("c\000") || key.startsWith("f\000") || key.startsWith("m\000")) { + signatures.put(key, printout); + } + else { + classPatterns.add(new ClassPatternRule(key, message)); + } + } + } + } + +private Collection getKeys(final UnresolvableReporting report, final boolean localIgnoreMissingClasses, final Set missingClasses, + final String signature) throws ParseException, IOException { + final String clazz; + final String field; + final Method method; + int p; p = signature.indexOf('#'); if (p >= 0) { clazz = signature.substring(0, p); @@ -170,31 +209,28 @@ private void addSignature(final String line, final String defaultMessage, final method = null; field = null; } - if (message != null && message.isEmpty()) { - message = null; - } - // create printout message: - final String printout = (message != null) ? (signature + " [" + message + "]") : signature; + // check class & method/field signature, if it is really existent (in classpath), but we don't really load the class into JVM: if (AsmUtils.isGlob(clazz)) { if (method != null || field != null) { throw new ParseException(String.format(Locale.ENGLISH, "Class level glob pattern cannot be combined with methods/fields: %s", signature)); } - classPatterns.add(new ClassPatternRule(clazz, message)); + return Collections.singleton(clazz); } else { final ClassMetadata c; + Collection keys = new ArrayList<>(); try { c = lookup.getClassFromClassLoader(clazz); } catch (ClassNotFoundException cnfe) { if (this.ignoreSignaturesOfMissingClasses || localIgnoreMissingClasses) { - return; + return null; } if (report.reportClassNotFound) { report.parseFailed(logger, String.format(Locale.ENGLISH, "Class '%s' not found on classpath", cnfe.getMessage()), signature); } else { missingClasses.add(clazz); } - return; + return null; } if (method != null) { assert field == null; @@ -204,28 +240,29 @@ private void addSignature(final String line, final String defaultMessage, final if (m.getName().equals(method.getName()) && (WILDCARD_ARGS.equals(method.getDescriptor()) || Arrays.equals(m.getArgumentTypes(), method.getArgumentTypes()))) { found = true; - signatures.put(getKey(c.className, m), printout); + keys.add(getKey(c.className, m)); // don't break when found, as there may be more covariant overrides! } } if (!found) { report.parseFailed(logger, "Method not found", signature); - return; + return null; } } else if (field != null) { assert method == null; if (!c.fields.contains(field)) { report.parseFailed(logger, "Field not found", signature); - return; + return null; } - signatures.put(getKey(c.className, field), printout); + keys.add(getKey(c.className, field)); } else { assert field == null && method == null; // only add the signature as class name - signatures.put(getKey(c.className), printout); + keys.add(getKey(c.className)); } + return keys; } - } +} private void reportMissingSignatureClasses(Set missingClasses) { if (missingClasses.isEmpty()) { @@ -337,6 +374,26 @@ public boolean noSignaturesFilesParsed() { return numberOfFiles == 0; } + public void setSignaturesSeverity(Collection signature, ViolationSeverity severity) throws ParseException, IOException { + logger.info("Adjusting severity to " + severity + " for signatures..."); + for (String s : signature) { + setSignatureSeverity(s, severity); + } + } + + public void setSignatureSeverity(String signature, ViolationSeverity severity) throws ParseException, IOException { + Collection keys = getKeys(UnresolvableReporting.SILENT, false, new HashSet(), signature); + if (keys != null) { + for (String key : keys) { + if (key.startsWith("c\000") || key.startsWith("f\000") || key.startsWith("m\000")) { + severityPerSignature.put(key, severity); + } else { + severityPerClassPattern.put(AsmUtils.glob2Pattern(key), severity); + } + } + } + } + /** Returns if bundled signature to enable heuristics for detection of non-portable runtime calls is used */ public boolean isNonPortableRuntimeForbidden() { return this.forbidNonPortableRuntime; @@ -346,33 +403,63 @@ private static String formatTypePrintout(String printout, String what) { return String.format(Locale.ENGLISH, "Forbidden %s use: %s", what, printout); } - public String checkType(Type type, String what) { + /** + * Represents a violation (usage of a forbidden method/field/class). + * Encapsulates both message and severity. + */ + public static class ViolationResult { + public final String message; + public final ViolationSeverity severity; + + public ViolationResult(String message, ViolationSeverity severity) { + this.message = message; + this.severity = severity; + } + } + + public ViolationResult checkType(Type type, String what) { if (type.getSort() != Type.OBJECT) { return null; // we don't know this type, just pass! } + final String key = getKey(type.getInternalName()); final String printout = signatures.get(getKey(type.getInternalName())); if (printout != null) { - return formatTypePrintout(printout, what); + return new ViolationResult(formatTypePrintout(printout, what), getSeverityForKey(key)); } final String binaryClassName = type.getClassName(); for (final ClassPatternRule r : classPatterns) { if (r.matches(binaryClassName)) { - return formatTypePrintout(r.getPrintout(binaryClassName), what); + return new ViolationResult(formatTypePrintout(r.getPrintout(binaryClassName), what), getSeverityForClassName(binaryClassName)); } } return null; } - public String checkMethod(String internalClassName, Method method) { - final String printout = signatures.get(getKey(internalClassName, method)); - return (printout == null) ? null : "Forbidden method invocation: ".concat(printout); + public ViolationResult checkMethod(String internalClassName, Method method) { + final String key = getKey(internalClassName, method); + final String printout = signatures.get(key); + return (printout == null) ? null : new ViolationResult("Forbidden method invocation: ".concat(printout), getSeverityForKey(key)); } - public String checkField(String internalClassName, String field) { - final String printout = signatures.get(getKey(internalClassName, field)); - return (printout == null) ? null : "Forbidden field access: ".concat(printout); + public ViolationResult checkField(String internalClassName, String field) { + final String key = getKey(internalClassName, field); + final String printout = signatures.get(key); + return (printout == null) ? null : new ViolationResult("Forbidden field access: ".concat(printout), getSeverityForKey(key)); } - + + private ViolationSeverity getSeverityForKey(String key) { + return severityPerSignature.getOrDefault(key, failOnViolation ? ViolationSeverity.ERROR : ViolationSeverity.WARNING); + } + + private ViolationSeverity getSeverityForClassName(String className) { + for (final Map.Entry e : severityPerClassPattern.entrySet()) { + if (e.getKey().matcher(className).matches()) { + return e.getValue(); + } + } + return failOnViolation ? ViolationSeverity.ERROR : ViolationSeverity.WARNING; + } + public static String fixTargetVersion(String name) throws ParseException { final Matcher m = JDK_SIG_PATTERN.matcher(name); if (m.matches()) { diff --git a/src/main/java/de/thetaphi/forbiddenapis/ant/AntTask.java b/src/main/java/de/thetaphi/forbiddenapis/ant/AntTask.java index b93b198..48e34f9 100644 --- a/src/main/java/de/thetaphi/forbiddenapis/ant/AntTask.java +++ b/src/main/java/de/thetaphi/forbiddenapis/ant/AntTask.java @@ -20,6 +20,7 @@ import static de.thetaphi.forbiddenapis.Checker.Option.*; +import org.apache.maven.plugins.annotations.Parameter; import org.apache.tools.ant.AntClassLoader; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; @@ -43,6 +44,7 @@ import java.io.IOException; import java.io.File; +import java.util.Arrays; import java.util.Collection; import java.util.EnumSet; import java.util.Iterator; @@ -61,6 +63,9 @@ public class AntTask extends Task implements Constants { private final Union apiSignatures = new Union(); private final Collection bundledSignatures = new LinkedHashSet<>(); private final Collection suppressAnnotations = new LinkedHashSet<>(); + private final Collection signaturesWithSeverityWarn = new LinkedHashSet<>();; + private final Collection signaturesWithSeveritySuppress = new LinkedHashSet<>();; + private Path classpath = null; private boolean failOnUnsupportedJava = false; @@ -174,6 +179,12 @@ public void debug(String msg) { checker.parseSignaturesFile(r.getInputStream(), r.toString()); } } + if (!signaturesWithSeverityWarn.isEmpty()) { + checker.setSignaturesSeverity(signaturesWithSeverityWarn, Checker.ViolationSeverity.WARNING); + } + if (!signaturesWithSeveritySuppress.isEmpty()) { + checker.setSignaturesSeverity(signaturesWithSeveritySuppress, Checker.ViolationSeverity.SUPPRESS); + } } catch (IOException ioe) { throw new BuildException("IO problem while reading files with API signatures: " + ioe.getMessage(), ioe); } catch (ParseException pe) { @@ -289,6 +300,23 @@ public void setBundledSignatures(String name) { createBundledSignatures().setName(name); } + /** + * A list of forbidden API signatures for which violations should not be reported at all (i.e. neither fail the build nor appear in the logs). This takes precedence over {@link #failOnViolation} and {@link #signaturesWithSeverityWarn}. + * In order to be effective the signature must be given in either {@link #bundledSignatures}, {@link #signaturesFiles}, {@link #signaturesArtifacts}, or {@link #signatures}. + * @since 3.9 + */ + public void setSignaturesWithSeverityWarn(String signature) { + signaturesWithSeverityWarn.add(signature); + } + + /** A list of forbidden API signatures for which violations should not be reported at all (i.e. neither fail the build nor appear in the logs). This takes precedence over {@link #failOnViolation} and {@link #signaturesWithSeverityWarn}. + * In order to be effective the signature must be given in either {@link #bundledSignatures}, {@link #signaturesFiles}, {@link #signaturesArtifacts}, or {@link #signatures}. + * @since 3.9 + */ + public void setSignaturesWithSeveritySuppress(String signature) { + signaturesWithSeveritySuppress.add(signature); + } + /** Creates a instance of an annotation class name that suppresses error reporting in classes/methods/fields. */ public SuppressAnnotationType createSuppressAnnotation() { final SuppressAnnotationType s = new SuppressAnnotationType(); diff --git a/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java b/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java index 8e6f275..e3c75f0 100644 --- a/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java +++ b/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java @@ -53,8 +53,9 @@ public final class CliMain implements Constants { private final Logger logger; - private final Option classpathOpt, dirOpt, includesOpt, excludesOpt, signaturesfileOpt, bundledsignaturesOpt, suppressannotationsOpt, - allowmissingclassesOpt, ignoresignaturesofmissingclassesOpt, allowunresolvablesignaturesOpt, versionOpt, helpOpt, debugOpt; + private final Option classpathOpt, dirOpt, includesOpt, excludesOpt, signaturesfileOpt, bundledsignaturesOpt, signatureswithseveritysuppressOpt, + signatureswithseveritywarnOpt, suppressannotationsOpt, allowmissingclassesOpt, ignoresignaturesofmissingclassesOpt, allowunresolvablesignaturesOpt, + versionOpt, helpOpt, debugOpt; private final CommandLine cmd; public static final int EXIT_SUCCESS = 0; @@ -121,6 +122,20 @@ public CliMain(String... args) throws ExitException { .valueSeparator(',') .argName("name") .build()); + options.addOption(signatureswithseveritysuppressOpt = Option.builder() + .desc("forbidden API signature for which violations should not be reported at all (separated by commas or option can be given multiple times)") + .longOpt("signatureswithseveritysuppress") + .hasArgs() + .valueSeparator(',') + .argName("name") + .build()); + options.addOption(signatureswithseveritywarnOpt = Option.builder() + .desc("forbidden API signature for which violations just be reported at warn level but not lead to a non-success exit code (separated by commas or option can be given multiple times)") + .longOpt("signatureswithseveritywarn") + .hasArgs() + .valueSeparator(',') + .argName("name") + .build()); options.addOption(suppressannotationsOpt = Option.builder() .desc("class name or glob pattern of annotation that suppresses error reporting in classes/methods/fields (separated by commas or option can be given multiple times)") .longOpt("suppressannotation") @@ -140,7 +155,7 @@ public CliMain(String... args) throws ExitException { .desc("DEPRECATED: don't fail if a signature is not resolving") .longOpt("allowunresolvablesignatures") .build()); - + try { this.cmd = new DefaultParser().parse(options, args); final boolean debugLogging = cmd.hasOption(debugOpt.getLongOpt()); @@ -276,6 +291,14 @@ public void run() throws ExitException { final File f = new File(sf).getAbsoluteFile(); checker.parseSignaturesFile(f); } + final String[] signaturesWithSeverityWarn = cmd.getOptionValues(signatureswithseveritywarnOpt.getLongOpt()); + if (signaturesWithSeverityWarn != null) { + checker.setSignaturesSeverity(Arrays.asList(signaturesWithSeverityWarn), Checker.ViolationSeverity.WARNING); + } + final String[] signaturesWithSeveritySuppress = cmd.getOptionValues(signatureswithseveritysuppressOpt.getLongOpt()); + if (signaturesWithSeveritySuppress != null) { + checker.setSignaturesSeverity(Arrays.asList(signaturesWithSeveritySuppress), Checker.ViolationSeverity.SUPPRESS); + } } catch (IOException ioe) { throw new ExitException(EXIT_ERR_OTHER, "IO problem while reading files with API signatures: " + ioe); } catch (ParseException pe) { diff --git a/src/main/java/de/thetaphi/forbiddenapis/gradle/CheckForbiddenApis.java b/src/main/java/de/thetaphi/forbiddenapis/gradle/CheckForbiddenApis.java index 5b3de23..6b695b7 100644 --- a/src/main/java/de/thetaphi/forbiddenapis/gradle/CheckForbiddenApis.java +++ b/src/main/java/de/thetaphi/forbiddenapis/gradle/CheckForbiddenApis.java @@ -25,6 +25,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; +import java.util.Arrays; import java.util.EnumSet; import java.util.LinkedHashSet; import java.util.List; @@ -32,6 +33,7 @@ import java.util.Objects; import java.util.Set; +import org.apache.maven.plugins.annotations.Parameter; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; import org.gradle.api.InvalidUserDataException; @@ -231,7 +233,38 @@ public Set getBundledSignatures() { public void setBundledSignatures(Set bundledSignatures) { data.bundledSignatures = bundledSignatures; } + /** + *Aa list of forbidden API signatures for which violations should not be reported at all (i.e. neither fail the build nor appear in the logs). This takes precedence over {@link #getFailOnViolation()} and {@link #getSignaturesWithSeverityWarn()}. + * In order to be effective the signature must be given in either {@link #getSignaturesFiles()}, {@link #getBundledSignatures()}, or {@link #getSignatures}. + * @since 3.9 + */ + @Input + @Optional + public Set getSignaturesWithSeveritySuppress() { + return data.signaturesWithSeveritySuppress; + } + /** @see #getSignaturesWithSeveritySuppress */ + public void setSignaturesWithSeveritySuppress(Set signatures) { + data.signaturesWithSeveritySuppress = signatures; + } + + /** + * A list of forbidden API signatures for which violations should lead to a warning only (i.e. not fail the build). This takes precedence over {@link #getFailOnViolation()}. + * In order to be effective the signature must be given in either {@link #getSignaturesFiles()}, {@link #getBundledSignatures()}, or {@link #getSignatures}. + * @since 3.9 + */ + @Input + @Optional + public Set getSignaturesWithSeverityWarn() { + return data.signaturesWithSeverityWarn; + } + + /** @see #getSignaturesWithSeverityWarj */ + public void setSignaturesWithSeverityWarn(Set signatures) { + data.signaturesWithSeverityWarn = signatures; + } + /** * Fail the build, if the bundled ASM library cannot read the class file format * of the runtime library or the runtime library cannot be discovered. @@ -597,6 +630,14 @@ public void debug(String msg) { } checker.parseSignaturesString(sb.toString()); } + Set signaturesWithSeverityWarn = getSignaturesWithSeverityWarn(); + if (signaturesWithSeverityWarn != null && !signaturesWithSeverityWarn.isEmpty()) { + checker.setSignaturesSeverity(signaturesWithSeverityWarn, Checker.ViolationSeverity.WARNING); + } + Set signaturesWithSeveritySuppress = getSignaturesWithSeveritySuppress(); + if (signaturesWithSeveritySuppress != null && !signaturesWithSeveritySuppress.isEmpty()) { + checker.setSignaturesSeverity(signaturesWithSeveritySuppress, Checker.ViolationSeverity.SUPPRESS); + } } catch (IOException ioe) { throw new GradleException("IO problem while reading files with API signatures.", ioe); } catch (ParseException pe) { diff --git a/src/main/java/de/thetaphi/forbiddenapis/gradle/CheckForbiddenApisExtension.java b/src/main/java/de/thetaphi/forbiddenapis/gradle/CheckForbiddenApisExtension.java index 6b92fa3..9adb140 100644 --- a/src/main/java/de/thetaphi/forbiddenapis/gradle/CheckForbiddenApisExtension.java +++ b/src/main/java/de/thetaphi/forbiddenapis/gradle/CheckForbiddenApisExtension.java @@ -41,7 +41,9 @@ public CheckForbiddenApisExtension(Project project) { public Set signaturesURLs = new LinkedHashSet<>(); public List signatures = new ArrayList<>(); public Set bundledSignatures = new LinkedHashSet<>(), - suppressAnnotations = new LinkedHashSet<>(); + suppressAnnotations = new LinkedHashSet<>(), + signaturesWithSeveritySuppress = new LinkedHashSet<>(), + signaturesWithSeverityWarn = new LinkedHashSet<>(); public boolean failOnUnsupportedJava = false, failOnMissingClasses = true, failOnUnresolvableSignatures = true, diff --git a/src/main/java/de/thetaphi/forbiddenapis/maven/AbstractCheckMojo.java b/src/main/java/de/thetaphi/forbiddenapis/maven/AbstractCheckMojo.java index 7cc79f6..2813077 100644 --- a/src/main/java/de/thetaphi/forbiddenapis/maven/AbstractCheckMojo.java +++ b/src/main/java/de/thetaphi/forbiddenapis/maven/AbstractCheckMojo.java @@ -116,6 +116,22 @@ public abstract class AbstractCheckMojo extends AbstractMojo implements Constant @Parameter(required = false) private String[] bundledSignatures; + /** + * Specifies a list of forbidden API signatures for which violations should lead to a warning only (i.e. not fail the build). This takes precedence over {@link #failOnViolation}. + * In order to be effective the signature must be given in either {@link #bundledSignatures}, {@link #signaturesFiles}, {@link #signaturesArtifacts}, or {@link #signatures}. + * @since 3.9 + */ + @Parameter(required = false) + private String[] signaturesWithSeverityWarn; + + /** + * Specifies a list of forbidden API signatures for which violations should not be reported at all (i.e. neither fail the build nor appear in the logs). This takes precedence over {@link #failOnViolation} and {@link #signaturesWithSeverityWarn}. + * In order to be effective the signature must be given in either {@link #bundledSignatures}, {@link #signaturesFiles}, {@link #signaturesArtifacts}, or {@link #signatures}. + * @since 3.9 + */ + @Parameter(required = false) + private String[] signaturesWithSeveritySuppress; + /** * Fail the build, if the bundled ASM library cannot read the class file format * of the runtime library or the runtime library cannot be discovered. @@ -451,6 +467,12 @@ public void debug(String msg) { if (sig != null && sig.length() != 0) { checker.parseSignaturesString(sig); } + if (signaturesWithSeverityWarn != null) { + checker.setSignaturesSeverity(Arrays.asList(signaturesWithSeverityWarn), Checker.ViolationSeverity.WARNING); + } + if (signaturesWithSeveritySuppress != null) { + checker.setSignaturesSeverity(Arrays.asList(signaturesWithSeveritySuppress), Checker.ViolationSeverity.SUPPRESS); + } } catch (IOException ioe) { throw new MojoExecutionException("IO problem while reading files with API signatures.", ioe); } catch (ParseException pe) { diff --git a/src/test/antunit/TestFailOnViolation.xml b/src/test/antunit/TestFailOnViolation.xml index f7dc998..f7ec54c 100644 --- a/src/test/antunit/TestFailOnViolation.xml +++ b/src/test/antunit/TestFailOnViolation.xml @@ -35,6 +35,6 @@ java.awt.Color @ Color is disallowed, thats not bad, because ANT has no colors... java.lang.String @ You are crazy that you disallow strings - + \ No newline at end of file diff --git a/src/test/antunit/TestMavenMojo.xml b/src/test/antunit/TestMavenMojo.xml index e68e5d3..d79c5ec 100644 --- a/src/test/antunit/TestMavenMojo.xml +++ b/src/test/antunit/TestMavenMojo.xml @@ -25,6 +25,7 @@ + @@ -68,7 +69,18 @@ - + + + + + + + + + + + + diff --git a/src/test/antunit/pom-generator.xsl b/src/test/antunit/pom-generator.xsl index 8ced350..13e2acb 100644 --- a/src/test/antunit/pom-generator.xsl +++ b/src/test/antunit/pom-generator.xsl @@ -56,6 +56,7 @@ jdk-system-out ${antunit.signatures} + @@ -83,6 +84,31 @@ + + + + withAdjustedSeverity + + + antunit.signatureWithSeveritySuppress + + + + + + ${groupId} + ${artifactId} + + + + ${antunit.signatureWithSeveritySuppress} + + + + + + +