Skip to content

Commit

Permalink
Added matchers to check the visibility (public/private) of various re…
Browse files Browse the repository at this point in the history
…flective elements.

This is helpful, for example, when enforcing the scope of a public-facing API with a test,
and provides stronger documentation for the future than mere comments.
  • Loading branch information
jbrown authored and brownian-motion committed Jul 1, 2021
1 parent 8522353 commit 14db37f
Show file tree
Hide file tree
Showing 9 changed files with 754 additions and 0 deletions.
57 changes: 57 additions & 0 deletions hamcrest/src/main/java/org/hamcrest/reflection/Visibility.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.hamcrest.reflection;

import java.lang.reflect.Member;
import java.lang.reflect.Modifier;
import java.util.Objects;

/**
* Represents the 4 states of visibility.
*
* @author JJ Brown
*/
enum Visibility {
PUBLIC("public"),
PROTECTED("protected"),
PACKAGE_PROTECTED("package-protected (no modifiers)"),
PRIVATE("private");

public String getDescription() {
return description;
}

private final String description;

Visibility(String description) {
this.description = description;
}

static Visibility of(Class<?> clazz) {
Objects.requireNonNull(clazz, "Cannot determine the visibility of a null-valued reflective Class object");

if (Modifier.isPublic(clazz.getModifiers())) {
return Visibility.PUBLIC;
}
if (Modifier.isProtected(clazz.getModifiers())) {
return Visibility.PROTECTED;
}
if (Modifier.isPrivate(clazz.getModifiers())) {
return Visibility.PRIVATE;
}
return Visibility.PACKAGE_PROTECTED;
}

static Visibility of(Member member) {
Objects.requireNonNull(member, "Cannot determine the visibility of a null-valued reflective member object");

if (Modifier.isPublic(member.getModifiers())) {
return Visibility.PUBLIC;
}
if (Modifier.isProtected(member.getModifiers())) {
return Visibility.PROTECTED;
}
if (Modifier.isPrivate(member.getModifiers())) {
return Visibility.PRIVATE;
}
return Visibility.PACKAGE_PROTECTED;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.hamcrest.reflection;

import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;

import java.lang.reflect.Member;

/**
* Matches the visibility of a reflective element, like a {@link Class} or a {@link java.lang.reflect.Method},
* to make assertions about the scope of a module's API.
* <p>
* This class is intentionally not exposed to the public API, to help keep implementation details hidden (and easy to change).
* Please use {@link VisibilityMatchers} to instantiate instances of this class.
*
* @param <T> the type of the element being matched; could be anything
* @author JJ Brown
* @see VisibilityMatchers
*/
class VisibilityMatcher<T> extends BaseMatcher<T> {
private final Visibility expectedVisibility;

VisibilityMatcher(Visibility expectedVisibility) {
this.expectedVisibility = expectedVisibility;
}

@Override
public boolean matches(Object actual) {
if (actual == null) {
return false;
}
if (actual instanceof Class) {
return expectedVisibility == Visibility.of((Class<?>) actual);
}
if (actual instanceof Member) {
return expectedVisibility == Visibility.of((Member) actual);
}
return false;
}

@Override
public void describeTo(Description description) {
description.appendText("is ").appendText(expectedVisibility.getDescription());
}

@Override
public void describeMismatch(Object item, Description description) {
if (item == null) {
description.appendText("was null");
} else if (item instanceof Class) {
description.appendText("was a ")
.appendText(Visibility.of((Class<?>) item).getDescription())
.appendText(" class");
} else if (item instanceof Member) {
description.appendText("was a ")
.appendText(Visibility.of((Member) item).getDescription())
.appendText(" ")
.appendText(item.getClass().getName());
} else {
description.appendText("was " + item.getClass().getName() + " instead of a reflective element like a Class<T>, Constructor<T>, or Method");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package org.hamcrest.reflection;

import org.hamcrest.Matcher;

/**
* Defines matchers that check the visibility of reflective objects like {@link java.lang.Class} or {@link java.lang.reflect.Method}.
* {@code null} values never match, nor do normal objects; these simply do not match, without raising an Exception.
*
* @author JJ Brown
*/
public class VisibilityMatchers {
// Each matcher is stateless and can match any type, so the individual instances are only made once and stored here for re-use.
private static final VisibilityMatcher<?> PUBLIC = new VisibilityMatcher<>(Visibility.PUBLIC);
private static final VisibilityMatcher<?> PROTECTED = new VisibilityMatcher<>(Visibility.PROTECTED);
private static final VisibilityMatcher<?> PACKAGE_PROTECTED = new VisibilityMatcher<>(Visibility.PACKAGE_PROTECTED);
private static final VisibilityMatcher<?> PRIVATE = new VisibilityMatcher<>(Visibility.PRIVATE);

/**
* Matchers reflective elements that have public visibility.
* Specifically, this matcher only matches elements marked with the keyword {@code public}.
* <br>
* This method matches {@link Class} objects or other {@link java.lang.reflect.Member reflective objects}
* like {@link java.lang.reflect.Field} or {@link java.lang.reflect.Method} used in reflection.
* Any other kind of object, or {@code null} values, do not match (but will not cause an Exception).
*
* @param <T> the type of the object being matched
* @return a matcher that matches reflective elements with exactly the given level of visibility
*/
@SuppressWarnings("unchecked")
public static <T> Matcher<T> isPublic() {
// Each matcher is stateless and can match any type (the generic <T> is for type safety at the use site),
// so it's fine to cast the non-reifiable generic type here at runtime and re-use the same instance.
return (Matcher<T>) PUBLIC;
}

/**
* Matchers reflective elements that have protected visibility.
* Specifically, this matcher only matches elements marked with the keyword {@code protected}; it does NOT match public or private elements.
* <br>
* This method matches {@link Class} objects or other {@link java.lang.reflect.Member reflective objects}
* like {@link java.lang.reflect.Field} or {@link java.lang.reflect.Method} used in reflection.
* Any other kind of object, or {@code null} values, do not match (but will not cause an Exception).
*
* @param <T> the type of the object being matched
* @return a matcher that matches reflective elements with exactly the given level of visibility
*/
@SuppressWarnings("unchecked")
public static <T> Matcher<T> isProtected() {
// Each matcher is stateless and can match any type (the generic <T> is for type safety at the use site),
// so it's fine to cast the non-reifiable generic type here at runtime and re-use the same instance.
return (Matcher<T>) PROTECTED;
}

/**
* Matchers reflective elements that have package-protected visibility.
* Specifically, this matcher only matches elements not marked with any of the visibility keywords {@code public}, {@code protected}, or {@code private}.
* <br>
* This method matches {@link Class} objects or other {@link java.lang.reflect.Member reflective objects}
* like {@link java.lang.reflect.Field} or {@link java.lang.reflect.Method} used in reflection.
* Any other kind of object, or {@code null} values, do not match (but will not cause an Exception).
*
* @param <T> the type of the object being matched
* @return a matcher that matches reflective elements with exactly the given level of visibility
*/
@SuppressWarnings("unchecked")
public static <T> Matcher<T> isPackageProtected() {
// Each matcher is stateless and can match any type (the generic <T> is for type safety at the use site),
// so it's fine to cast the non-reifiable generic type here at runtime and re-use the same instance.
return (Matcher<T>) PACKAGE_PROTECTED;
}

/**
* Matchers reflective elements that have private visibility.
* Specifically, this matcher only matches elements marked with the keyword {@link private}.
* <br>
* This method matches {@link Class} objects or other {@link java.lang.reflect.Member reflective objects}
* like {@link java.lang.reflect.Field} or {@link java.lang.reflect.Method} used in reflection.
* Any other kind of object, or {@code null} values, do not match (but will not cause an Exception).
*
* @param <T> the type of the object being matched
* @return a matcher that matches reflective elements with exactly the given level of visibility
*/
@SuppressWarnings("unchecked")
public static <T> Matcher<T> isPrivate() {
// Each matcher is stateless and can match any type (the generic <T> is for type safety at the use site),
// so it's fine to cast the non-reifiable generic type here at runtime and re-use the same instance.
return (Matcher<T>) PRIVATE;
}
}
11 changes: 11 additions & 0 deletions hamcrest/src/main/java/org/hamcrest/reflection/package.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<p>Matchers that perform checks on reflective elements, such as Class&lt;?&gt; and Method&lt;?&gt; objects.</p>
<p>This provides tools to enforce boundaries about the scope of visible items in a module,
and to explicitly ensure that items are available to reflection when they may only be loaded at runtime.</p>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package org.hamcrest.reflection;


import org.hamcrest.AbstractMatcherTest;
import org.hamcrest.Matcher;
import org.junit.Test;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

import static org.hamcrest.reflection.VisibilityMatchers.isPackageProtected;
import static org.hamcrest.reflection.VisibilityMatchers.isPublic;

@SuppressWarnings("unused")
public class IsPackageProtectedTest extends AbstractMatcherTest {
@Override
protected Matcher<?> createMatcher() {
return isPackageProtected();
}

@Test
public void test_packageExposesPublicFactoryMethod() throws NoSuchMethodException {
assertMatches(isPublic(), VisibilityMatchers.class.getMethod("isPackageProtected"));
}

@Test
public void test_isPackageProtected_matchesOnlyPackageProtectedClasses() {
assertDoesNotMatch(isPackageProtected(), PublicClass.class);
assertDoesNotMatch(isPackageProtected(), ProtectedClass.class);
assertMatches(isPackageProtected(), PackageProtectedClass.class);
assertDoesNotMatch(isPackageProtected(), PrivateClass.class);

assertDescription("is package-protected (no modifiers)", isPackageProtected());

assertMismatchDescription("was a public class", isPackageProtected(), PublicClass.class);
assertMismatchDescription("was a protected class", isPackageProtected(), ProtectedClass.class);
assertMismatchDescription("was a private class", isPackageProtected(), PrivateClass.class);
}


@Test
public void test_isPackageProtected_matchesOnlyPackageProtectedFields() throws NoSuchFieldException {
Field publicField = ExampleFields.class.getDeclaredField("publicField");
Field protectedField = ExampleFields.class.getDeclaredField("protectedField");
Field packageProtectedField = ExampleFields.class.getDeclaredField("packageProtectedField");
Field privateField = ExampleFields.class.getDeclaredField("privateField");

assertDoesNotMatch(isPackageProtected(), publicField);
assertDoesNotMatch(isPackageProtected(), protectedField);
assertMatches(isPackageProtected(), packageProtectedField);
assertDoesNotMatch(isPackageProtected(), privateField);

assertDescription("is package-protected (no modifiers)", isPackageProtected());

assertMismatchDescription("was a public java.lang.reflect.Field", isPackageProtected(), publicField);
assertMismatchDescription("was a protected java.lang.reflect.Field", isPackageProtected(), protectedField);
assertMismatchDescription("was a private java.lang.reflect.Field", isPackageProtected(), privateField);
}


@Test
public void test_isPackageProtected_matchesOnlyPackageProtectedMethods() throws NoSuchMethodException {
Method publicMethod = ExampleMethods.class.getDeclaredMethod("publicMethod");
Method protectedMethod = ExampleMethods.class.getDeclaredMethod("protectedMethod");
Method packageProtectedMethod = ExampleMethods.class.getDeclaredMethod("packageProtectedMethod");
Method privateMethod = ExampleMethods.class.getDeclaredMethod("privateMethod");

assertDoesNotMatch(isPackageProtected(), publicMethod);
assertDoesNotMatch(isPackageProtected(), protectedMethod);
assertMatches(isPackageProtected(), packageProtectedMethod);
assertDoesNotMatch(isPackageProtected(), privateMethod);

assertDescription("is package-protected (no modifiers)", isPackageProtected());

assertMismatchDescription("was a public java.lang.reflect.Method", isPackageProtected(), publicMethod);
assertMismatchDescription("was a protected java.lang.reflect.Method", isPackageProtected(), protectedMethod);
assertMismatchDescription("was a private java.lang.reflect.Method", isPackageProtected(), privateMethod);
}

@Test
public void test_isPackageProtected_doesNotMatchNull() {
assertDoesNotMatch(isPackageProtected(), null);

assertMismatchDescription("was null", isPackageProtected(), null);
}

@Test
public void test_isPackageProtected_doesNotMatchNonReflectiveElement() {
assertDoesNotMatch(isPackageProtected(), new Object());

assertMismatchDescription("was java.lang.Object instead of a reflective element like a Class<T>, Constructor<T>, or Method", isPackageProtected(), new Object());
}

public static class PublicClass {
}

protected static class ProtectedClass {
}

static class PackageProtectedClass {
}

private static class PrivateClass {
}

private static class ExampleFields {
public Void publicField;
protected Void protectedField;
Void packageProtectedField;
private Void privateField;
}

private static class ExampleMethods {
public void publicMethod() {
}

protected void protectedMethod() {
}

void packageProtectedMethod() {
}

private void privateMethod() {
}
}
}
Loading

0 comments on commit 14db37f

Please sign in to comment.