Skip to content

Commit

Permalink
Wildcard method signatures (#188)
Browse files Browse the repository at this point in the history
Co-authored-by: Mike Drob <[email protected]>
Co-authored-by: Uwe Schindler <[email protected]>
  • Loading branch information
3 people authored Dec 16, 2021
1 parent d746151 commit 7f7767c
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 12 deletions.
7 changes: 4 additions & 3 deletions src/main/docs/signatures-syntax.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ <h1>Syntax of Custom Signatures Files</h1>
<li><em>A field of a class:</em> <code>package.Class#fieldName</code></li>
<li><em>A method signature:</em> It consists of a binary class name, followed by <code>#</code>
and a method name including method parameters: <code>java.lang.String#concat(java.lang.String)</code>
&ndash; All method parameters need to use fully qualified class names!
To refer to instance constructors, use the method name <code>&lt;init&gt;</code>,
e.g. <code>java.lang.Integer#&lt;init&gt;(int)</code>.</li>
&ndash; All method parameters need to use fully qualified class names! Instead of
method parameters, the special wildcard string <code>**</code> may be used to add all variants
of a method, regardless of their parameter types. To refer to instance constructors, use the
method name <code>&lt;init&gt;</code>, e.g. <code>java.lang.Integer#&lt;init&gt;(int)</code>.</li>
</ul>

<p>The error message displayed when the signature matches can be given at the end of each
Expand Down
28 changes: 19 additions & 9 deletions src/main/java/de/thetaphi/forbiddenapis/Signatures.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.objectweb.asm.Type;
import org.objectweb.asm.commons.Method;
Expand All @@ -48,6 +49,9 @@ public final class Signatures implements Constants {
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 final String WILDCARD_ARGS = "**";
private static final Pattern PATTERN_WILDCARD_ARGS = Pattern.compile(String.format(Locale.ROOT, "%s\\s*%s\\s*%s",
Pattern.quote("("), Pattern.quote(WILDCARD_ARGS), Pattern.quote(")")));

private static enum UnresolvableReporting {
FAIL(true) {
Expand Down Expand Up @@ -139,21 +143,26 @@ private void addSignature(final String line, final String defaultMessage, final
p = signature.indexOf('#');
if (p >= 0) {
clazz = signature.substring(0, p);
final String s = signature.substring(p + 1);
p = s.indexOf('(');
final String methodOrField = signature.substring(p + 1);
p = methodOrField.indexOf('(');
if (p >= 0) {
if (p == 0) {
throw new ParseException("Invalid method signature (method name missing): " + signature);
}
// we ignore the return type, its just to match easier (so return type is void):
try {
method = Method.getMethod("void " + s, true);
} catch (IllegalArgumentException iae) {
throw new ParseException("Invalid method signature: " + signature);
if (PATTERN_WILDCARD_ARGS.matcher(methodOrField.substring(p)).matches()) {
// we create a method instance with the special descriptor string "**", which gets detected later:
method = new Method(methodOrField.substring(0, p).trim(), WILDCARD_ARGS);
} else {
// we ignore the return type, it just allows the parser to succeed (so return type is void):
try {
method = Method.getMethod("void ".concat(methodOrField), true);
} catch (IllegalArgumentException iae) {
throw new ParseException("Invalid method signature: " + signature);
}
}
field = null;
} else {
field = s;
field = methodOrField;
method = null;
}
} else {
Expand Down Expand Up @@ -192,7 +201,8 @@ private void addSignature(final String line, final String defaultMessage, final
// list all methods with this signature:
boolean found = false;
for (final Method m : c.methods) {
if (m.getName().equals(method.getName()) && Arrays.equals(m.getArgumentTypes(), method.getArgumentTypes())) {
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);
// don't break when found, as there may be more covariant overrides!
Expand Down
10 changes: 10 additions & 0 deletions src/test/antunit/TestInlineSignatures.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@
<au:assertLogContains level="error" text="java.lang.String#substring(int,int) [You are crazy that you disallow substrings]"/>
</target>

<target name="testForbiddenWildcardMethodWithMessage">
<au:expectfailure expectedMessage="Check for forbidden API calls failed, see log">
<forbiddenapis classpathref="path.all">
<fileset refid="main.classes"/>
java.lang.String#substring(**) @ You are crazy that you disallow all substrings
</forbiddenapis>
</au:expectfailure>
<au:assertLogContains level="error" text="java.lang.String#substring(**) [You are crazy that you disallow all substrings]"/>
</target>

<target name="testForbiddenFieldWithMessage">
<au:expectfailure expectedMessage="Check for forbidden API calls failed, see log">
<forbiddenapis classpathref="path.all">
Expand Down
56 changes: 56 additions & 0 deletions src/test/java/de/thetaphi/forbiddenapis/CheckerSetupTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,62 @@ public void testMethodSignature() throws Exception {
assertFalse(checker.noSignaturesFilesParsed());
}

@Test
public void testMethodSignatureWS() throws Exception {
checker.parseSignaturesString("java.lang.Object# toString\t ( ) @ Foobar");
assertEquals(Collections.singletonMap(Signatures.getKey("java/lang/Object", new Method("toString", "()Ljava/lang/String;")), "java.lang.Object# toString\t ( ) [Foobar]"),
forbiddenSignatures.signatures);
assertEquals(Collections.emptySet(), forbiddenSignatures.classPatterns);
assertFalse(checker.hasNoSignatures());
assertFalse(checker.noSignaturesFilesParsed());
}

@Test
public void testWildcardMethodSignature() throws Exception {
checker.parseSignaturesString("java.lang.String#copyValueOf(**) @ Foobar");

// For Java 7 it should at least contain those 2 signatures:
assertTrue(forbiddenSignatures.signatures.containsKey(Signatures.getKey("java/lang/String", new Method("copyValueOf", "([C)Ljava/lang/String;"))));
assertTrue(forbiddenSignatures.signatures.containsKey(Signatures.getKey("java/lang/String", new Method("copyValueOf", "([CII)Ljava/lang/String;"))));

assertEquals(Collections.emptySet(), forbiddenSignatures.classPatterns);
assertFalse(checker.hasNoSignatures());
assertFalse(checker.noSignaturesFilesParsed());
}

@Test
public void testWildcardMethodSignatureWS() throws Exception {
checker.parseSignaturesString("java.lang.String#copyValueOf ( ** \t ) @ Foobar");

// For Java 7 it should at least contain those 2 signatures:
assertTrue(forbiddenSignatures.signatures.containsKey(Signatures.getKey("java/lang/String", new Method("copyValueOf", "([C)Ljava/lang/String;"))));
assertTrue(forbiddenSignatures.signatures.containsKey(Signatures.getKey("java/lang/String", new Method("copyValueOf", "([CII)Ljava/lang/String;"))));

assertEquals(Collections.emptySet(), forbiddenSignatures.classPatterns);
assertFalse(checker.hasNoSignatures());
assertFalse(checker.noSignaturesFilesParsed());
}

@Test
public void testWildcardMethodSignatureNoArgs() throws Exception {
checker.parseSignaturesString("java.lang.Object#toString(**) @ Foobar");
assertEquals(Collections.singletonMap(Signatures.getKey("java/lang/Object", new Method("toString", "()Ljava/lang/String;")), "java.lang.Object#toString(**) [Foobar]"),
forbiddenSignatures.signatures);
assertEquals(Collections.emptySet(), forbiddenSignatures.classPatterns);
assertFalse(checker.hasNoSignatures());
assertFalse(checker.noSignaturesFilesParsed());
}

@Test
public void testWildcardMethodSignatureNotExist() throws Exception {
try {
checker.parseSignaturesString("java.lang.Object#foobarNotExist(**) @ Foobar");
fail("Should fail to parse because method does not exist");
} catch (ParseException pe) {
assertEquals("Method not found while parsing signature: java.lang.Object#foobarNotExist(**)", pe.getMessage());
}
}

@Test
public void testEmptyCtor() throws Exception {
Checker chk = new Checker(StdIoLogger.INSTANCE, ClassLoader.getSystemClassLoader());
Expand Down

0 comments on commit 7f7767c

Please sign in to comment.