Skip to content

Commit

Permalink
Add support for glob patterns in suppress annotations. This closes #53
Browse files Browse the repository at this point in the history
  • Loading branch information
uschindler committed Apr 11, 2015
1 parent e1b0028 commit 66daabd
Show file tree
Hide file tree
Showing 9 changed files with 77 additions and 68 deletions.
6 changes: 4 additions & 2 deletions src/main/docs/ant-task.html
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ <h2>Parameters</h2>
<code>RetentionPolicy#CLASS</code>. It can be applied to classes, their methods,
or fields. By default, <code>@de.thetaphi.forbiddenapis.SuppressForbidden</code>
can always be used, but needs the <code>forbidden-apis.jar</code> file in classpath
of compiled project, which may not be wanted.</td>
of compiled project, which may not be wanted. Instead of a full class name, a glob
pattern may be used (e.g., <code>**.SuppressForbidden</code>).</td>
</tr>

</table>
Expand All @@ -164,7 +165,8 @@ <h2>Parameters specified as nested elements</h2>
<p>You can include multiple <code>&lt;suppressAnnotation classname=&quot;...&quot;&gt;</code> elements to specify class names of custom Java annotations that are used in the checked
code to suppress errors. Those annotations must have at least
<code>RetentionPolicy#CLASS</code>. They can be applied to classes, their methods, or fields. By default, <code>@de.thetaphi.forbiddenapis.SuppressForbidden</code>
can always be used, but needs the <code>forbidden-apis.jar</code> file in classpath of compiled project, which may not be wanted.</p>
can always be used, but needs the <code>forbidden-apis.jar</code> file in classpath of compiled project, which may not be wanted. Instead of a full class name, a glob
pattern may be used (e.g., <code>**.SuppressForbidden</code>).</p>

</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ public abstract class AbstractCheckMojo extends AbstractMojo {
* or fields. By default, {@code @de.thetaphi.forbiddenapis.SuppressForbidden}
* can always be used, but needs the {@code forbidden-apis.jar} file in classpath
* of compiled project, which may not be wanted.
* Instead of a full class name, a glob pattern may be used (e.g.,
* {@code **.SuppressForbidden}).
* @since 1.8
*/
@Parameter(required = false)
Expand Down
57 changes: 32 additions & 25 deletions src/main/java/de/thetaphi/forbiddenapis/AsmUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,35 +59,42 @@ public static boolean isGlob(String s) {
return s.indexOf('*') >= 0 || s.indexOf('?') >= 0;
}

/** Returns a regex pattern that matches the glob on class names (e.g., "sun.misc.**") */
public static Pattern glob2Pattern(String glob) {
/** Returns a regex pattern that matches on any of the globs on class names (e.g., "sun.misc.**") */
public static Pattern glob2Pattern(String... globs) {
final StringBuilder regex = new StringBuilder();
int i = 0, len = glob.length();
while (i < len) {
char c = glob.charAt(i++);
switch (c) {
case '*':
if (i < len && glob.charAt(i) == '*') {
// crosses package boundaries
regex.append(".*");
i++;
} else {
boolean needOr = false;
for (String glob : globs) {
if (needOr) {
regex.append('|');
}
int i = 0, len = glob.length();
while (i < len) {
char c = glob.charAt(i++);
switch (c) {
case '*':
if (i < len && glob.charAt(i) == '*') {
// crosses package boundaries
regex.append(".*");
i++;
} else {
// do not cross package boundaries
regex.append("[^.]*");
}
break;

case '?':
// do not cross package boundaries
regex.append("[^.]*");
}
break;
regex.append("[^.]");
break;

case '?':
// do not cross package boundaries
regex.append("[^.]");
break;

default:
if (isRegexMeta(c)) {
regex.append('\\');
}
regex.append(c);
default:
if (isRegexMeta(c)) {
regex.append('\\');
}
regex.append(c);
}
}
needOr = true;
}
return Pattern.compile(regex.toString(), 0);
}
Expand Down
20 changes: 8 additions & 12 deletions src/main/java/de/thetaphi/forbiddenapis/Checker.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -81,7 +80,7 @@ public abstract class Checker implements RelatedClassLookup {
// key is pattern to binary class name:
final Set<ClassPatternRule> forbiddenClassPatterns = new LinkedHashSet<ClassPatternRule>();
// descriptors (not internal names) of all annotations that suppress:
final Set<String> suppressAnnotations = new HashSet<String>();
final Set<String> suppressAnnotations = new LinkedHashSet<String>();

protected abstract void logError(String msg);
protected abstract void logWarn(String msg);
Expand Down Expand Up @@ -402,22 +401,18 @@ public final boolean hasNoSignatures() {

/** Adds the given annotation class for suppressing errors. */
public final void addSuppressAnnotation(Class<? extends Annotation> anno) {
suppressAnnotations.add(Type.getDescriptor(anno));
suppressAnnotations.add(anno.getName());
}

/** Adds suppressing annotation name in binary form (dotted). The class name is not checked for existence. */
/** Adds suppressing annotation name in binary form (dotted). It may also be a glob pattern. The class name is not checked for existence. */
public final void addSuppressAnnotation(String annoName) {
final Type type = Type.getObjectType(AsmUtils.binaryToInternal(annoName));
if (type.getSort() != Type.OBJECT) {
throw new IllegalArgumentException("Descriptor is not of OBJECT sort: " + type.getDescriptor());
}
suppressAnnotations.add(type.getDescriptor());
suppressAnnotations.add(annoName);
}

/** Parses a class and checks for valid method invocations */
private int checkClass(final ClassReader reader) {
private int checkClass(final ClassReader reader, Pattern suppressAnnotationsPattern) {
final String className = Type.getObjectType(reader.getClassName()).getClassName();
final ClassScanner scanner = new ClassScanner(this, forbiddenClasses, forbiddenClassPatterns, forbiddenMethods, forbiddenFields, suppressAnnotations, internalRuntimeForbidden);
final ClassScanner scanner = new ClassScanner(this, forbiddenClasses, forbiddenClassPatterns, forbiddenMethods, forbiddenFields, suppressAnnotationsPattern, internalRuntimeForbidden);
reader.accept(scanner, ClassReader.SKIP_FRAMES);
final List<ForbiddenViolation> violations = scanner.getSortedViolations();
final Pattern splitter = Pattern.compile(Pattern.quote("\n"));
Expand All @@ -431,9 +426,10 @@ private int checkClass(final ClassReader reader) {

public final void run() throws ForbiddenApiException {
int errors = 0;
final Pattern suppressAnnotationsPattern = AsmUtils.glob2Pattern(suppressAnnotations.toArray(new String[suppressAnnotations.size()]));
try {
for (final ClassSignature c : classesToCheck.values()) {
errors += checkClass(c.getReader());
errors += checkClass(c.getReader(), suppressAnnotationsPattern);
}
} catch (WrapperRuntimeException wre) {
Throwable cause = wre.getCause();
Expand Down
52 changes: 27 additions & 25 deletions src/main/java/de/thetaphi/forbiddenapis/ClassScanner.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
Expand Down Expand Up @@ -58,8 +58,8 @@ final class ClassScanner extends ClassVisitor {
final Map<String,String> forbiddenClasses;
// key is pattern to binary class name:
final Iterable<ClassPatternRule> forbiddenClassPatterns;
// descriptors (not internal names) of all annotations that suppress:
final Set<String> suppressAnnotations;
// pattern that matches binary (dotted) class name of all annotations that suppress:
final Pattern suppressAnnotations;

private String source = null;
private boolean isDeprecated = false;
Expand All @@ -77,7 +77,7 @@ final class ClassScanner extends ClassVisitor {
public ClassScanner(RelatedClassLookup lookup,
final Map<String,String> forbiddenClasses, final Iterable<ClassPatternRule> forbiddenClassPatterns,
final Map<String,String> forbiddenMethods, final Map<String,String> forbiddenFields,
final Set<String> suppressAnnotations,
final Pattern suppressAnnotations,
final boolean internalRuntimeForbidden) {
super(Opcodes.ASM5);
this.lookup = lookup;
Expand Down Expand Up @@ -215,19 +215,18 @@ String checkDescriptor(String desc) {
return checkType(Type.getType(desc));
}

String checkAnnotationDescriptor(String desc, boolean visible) {
final Type type = Type.getType(desc);
String checkAnnotationDescriptor(Type type, boolean visible) {
if (type.getSort() != Type.OBJECT) {
// should never happen for annotations!
throw new IllegalArgumentException("Annotation descriptor '" + desc + "' has wrong sort: " + type.getSort());
throw new IllegalArgumentException("Annotation descriptor '" + type.getDescriptor() + "' has wrong sort: " + type.getSort());
}
// for annotations, we don't need to look into super-classes, interfaces,...
// -> we just check if its disallowed or internal runtime (only if visible)!
return checkClassUse(type, "annotation", visible);
}

void maybeSuppressCurrentGroup(String annotationDesc) {
if (suppressAnnotations.contains(annotationDesc)) {
void maybeSuppressCurrentGroup(Type annotation) {
if (suppressAnnotations.matcher(annotation.getClassName()).matches()) {
suppressedGroups.set(currentGroupId);
}
}
Expand All @@ -244,7 +243,7 @@ public void visit(int version, int access, String name, String signature, String
this.isDeprecated = (access & Opcodes.ACC_DEPRECATED) != 0;
reportClassViolation(checkClassDefinition(superName, interfaces), "class declaration");
if (this.isDeprecated) {
classSuppressed |= suppressAnnotations.contains(DEPRECATED_DESCRIPTOR);
classSuppressed |= suppressAnnotations.matcher(DEPRECATED_TYPE.getClassName()).matches();
reportClassViolation(checkType(DEPRECATED_TYPE), "deprecation on class declaration");
}
}
Expand All @@ -260,14 +259,15 @@ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
// don't report 2 times!
return null;
}
classSuppressed |= suppressAnnotations.contains(desc);
reportClassViolation(checkAnnotationDescriptor(desc, visible), "annotation on class declaration");
final Type type = Type.getType(desc);
classSuppressed |= suppressAnnotations.matcher(type.getClassName()).matches();
reportClassViolation(checkAnnotationDescriptor(type, visible), "annotation on class declaration");
return null;
}

@Override
public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) {
reportClassViolation(checkAnnotationDescriptor(desc, visible), "type annotation on class declaration");
reportClassViolation(checkAnnotationDescriptor(Type.getType(desc), visible), "type annotation on class declaration");
return null;
}

Expand All @@ -285,7 +285,7 @@ public FieldVisitor visitField(final int access, final String name, final String
reportFieldViolation(checkDescriptor(desc), "field declaration");
}
if (this.isDeprecated) {
maybeSuppressCurrentGroup(DEPRECATED_DESCRIPTOR);
maybeSuppressCurrentGroup(DEPRECATED_TYPE);
reportFieldViolation(checkType(DEPRECATED_TYPE), "deprecation on field declaration");
}
}
Expand All @@ -296,14 +296,15 @@ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
// don't report 2 times!
return null;
}
maybeSuppressCurrentGroup(desc);
reportFieldViolation(checkAnnotationDescriptor(desc, visible), "annotation on field declaration");
final Type type = Type.getType(desc);
maybeSuppressCurrentGroup(type);
reportFieldViolation(checkAnnotationDescriptor(type, visible), "annotation on field declaration");
return null;
}

@Override
public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) {
reportFieldViolation(checkAnnotationDescriptor(desc, visible), "type annotation on field declaration");
reportFieldViolation(checkAnnotationDescriptor(Type.getType(desc), visible), "type annotation on field declaration");
return null;
}

Expand Down Expand Up @@ -332,7 +333,7 @@ public MethodVisitor visitMethod(final int access, final String name, final Stri
reportMethodViolation(checkDescriptor(desc), "method declaration");
}
if (this.isDeprecated) {
maybeSuppressCurrentGroup(DEPRECATED_DESCRIPTOR);
maybeSuppressCurrentGroup(DEPRECATED_TYPE);
reportMethodViolation(checkType(DEPRECATED_TYPE), "deprecation on method declaration");
}
}
Expand Down Expand Up @@ -428,38 +429,39 @@ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
// don't report 2 times!
return null;
}
maybeSuppressCurrentGroup(desc);
reportMethodViolation(checkAnnotationDescriptor(desc, visible), "annotation on method declaration");
final Type type = Type.getType(desc);
maybeSuppressCurrentGroup(type);
reportMethodViolation(checkAnnotationDescriptor(type, visible), "annotation on method declaration");
return null;
}

@Override
public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) {
reportMethodViolation(checkAnnotationDescriptor(desc, visible), "parameter annotation on method declaration");
reportMethodViolation(checkAnnotationDescriptor(Type.getType(desc), visible), "parameter annotation on method declaration");
return null;
}

@Override
public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) {
reportMethodViolation(checkAnnotationDescriptor(desc, visible), "type annotation on method declaration");
reportMethodViolation(checkAnnotationDescriptor(Type.getType(desc), visible), "type annotation on method declaration");
return null;
}

@Override
public AnnotationVisitor visitInsnAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) {
reportMethodViolation(checkAnnotationDescriptor(desc, visible), "annotation in method body");
reportMethodViolation(checkAnnotationDescriptor(Type.getType(desc), visible), "annotation in method body");
return null;
}

@Override
public AnnotationVisitor visitLocalVariableAnnotation(int typeRef, TypePath typePath, Label[] start, Label[] end, int[] index, String desc, boolean visible) {
reportMethodViolation(checkAnnotationDescriptor(desc, visible), "annotation in method body");
reportMethodViolation(checkAnnotationDescriptor(Type.getType(desc), visible), "annotation in method body");
return null;
}

@Override
public AnnotationVisitor visitTryCatchAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) {
reportMethodViolation(checkAnnotationDescriptor(desc, visible), "annotation in method body");
reportMethodViolation(checkAnnotationDescriptor(Type.getType(desc), visible), "annotation in method body");
return null;
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/de/thetaphi/forbiddenapis/CliMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public CliMain(String... args) throws ExitException {
.withArgName("name")
.create('b'));
options.addOption(suppressannotationsOpt = OptionBuilder
.withDescription("class name of annotation that suppresses error reporting in classes/methods/fields (separated by commas or option can be given multiple times)")
.withDescription("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)")
.withLongOpt("suppressannotation")
.hasArgs()
.withValueSeparator(',')
Expand Down
2 changes: 1 addition & 1 deletion src/test/antunit/TestAnnotations.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
<target name="testSuppressAnnotationsJava8">
<forbiddenapis failOnMissingClasses="true" classpath="${basedir}">
<!-- this should disable whole class scanning: -->
<suppressAnnotation classname="Java8Annotations$FooBar"/>
<suppressAnnotation classname="*$FooBar"/>
<!-- normally, this should produce similar errors like 'testAnnotations': -->
<file file="Java8Annotations.class"/>
Java8Annotations$ClassFileOnly @ Forbidden class-only annotation
Expand Down
2 changes: 1 addition & 1 deletion src/test/antunit/TestCli.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
<arg value="--includes"/>
<arg value="Java8Annotations.class"/>
<arg value="--suppressannotation"/>
<arg value="Java8Annotations$FooBar"/>
<arg value="*$FooBar"/>
</java>
<au:assertLogContains text=" 0 error(s)."/>
<au:assertLogContains text="Reading bundled API signatures: jdk-system-out"/>
Expand Down
2 changes: 1 addition & 1 deletion src/test/antunit/pom-incex-suppress.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
</excludes>
<suppressAnnotations>
<!-- this should suppress the whole tested class, so above signatures have no effect -->
<suppressAnnotation>Java8Annotations$FooBar</suppressAnnotation>
<suppressAnnotation>Java8Annotations$*</suppressAnnotation>
</suppressAnnotations>
</configuration>
<executions>
Expand Down

0 comments on commit 66daabd

Please sign in to comment.