Skip to content

Latest commit

 

History

History
309 lines (208 loc) · 14.3 KB

README.adoc

File metadata and controls

309 lines (208 loc) · 14.3 KB
refi

REFI, Some Reflection Tools

Member Selector and Filter Expressions

A filter expression is a logical expression. It is like a predicate that can be used to test java.lang.reflect.Method, java.lang.reflect.Field or java.lang.Class objects. The result is either true or false when evaluating an expression against a class or a class member. It may also throw a run-time exception if the expression is not valid for the member. For example, testing if a method is volatile, which is nonsense, the code will throw.

The expressions are readable strings, and they have to be compiled before executing.

You can use the Selector class to compile an expression and then to match it against a member, like

             Selector.compile("... expression ... ").match(myMember)

The result of the match will either be true or false.

The expression can check that the class, field, or method is private, public, abstract, volatile, and many other features. For example, the following line in the unit test checks that the inner class javax0.refi.selector.SutTargetClass.X is nested in a class with name starting with SutTarget.

assertTrue(Selector.compile("nestHost -> (!null & simpleName ~ /^SutTarget/)")
    .match(SutTargetClass.X.class));

We will list all the available primitive conditions in this document.

These primitive conditions can be used together as logical expressions containing logical AND, OR, and NOT operators, parentheses, and attribute conversions. We will talk about attribute conversion a bit later.

For example, the filter expression

public | private

will be true for any field, class, or method that is either private or public. It will be false for protected or package-private members.

You can use | to express OR relation and & to describe AND relation. For example, the filter expression

public | private & final

will select the public or private members, but the private fields also have to be final or will not be selected. (The operator & has higher precedence than |.)

The expressions can use the ! character to negate the following part. The ( and ) can also be used to override the evaluation order.

Sometimes you want to write a condition in part of the expression checking not the actual member but something related to that member. For example, you can write the expression

(returnType -> simpleName ~ /boolean/ | simpleName ~ /int/) & declaringClass -> !simpleName ~ /Object/

It will test that the simple name of the method’s return value is either boolean or int and that the declaring class is not the Object class. We explicitly check the method’s returnType in the first check. In this case, this is optional because the simple condition simpleName is also defined for methods. Methods only have names, but not simple names. Therefore simpleName is defined for methods to check the simple name of the return type. It is a shortcut that you can use to write the expression shorter. Before the | character, you could also write the first part without the returnType part.

The general advice in cases like this is that you should choose the more readable one per your taste, though be consistent. The example above is not consistent.

The checking against the actual names in all three cases are regex matchers. The ~ character separates the keyword and the regular expression. The regular expression is enclosed in / characters.

The regular expression is used invoking the java.util.regex.Matcher.find() method. It means that the above example by accident may match a method that has a return type containing the word boolean or int, for instance, Linter. To do an exact match, you have to write

          /^boolean$/
          /^int$/

The part declaringClass -> following the & signals will be evaluated not for the object but rather for the declaring class of the object. It is an attribute conversion. These conversions are on the same precedence level as the ! negation operator and can be used up to any level. For example, you can write

declaringClass -> superClass -> superClass -> !simpleName ~ /Object/

to check that the method is at least three inheritance levels deeper declared than the Object class.

Format Syntax of Filter Expressions

The formal BNF definition of the selector expressions is the following:

EXPRESSION ::= EXPRESSION1 ['|' EXPRESSION1 ]+
EXPRESSION1 ::= EXPRESSION2 ['&' EXPRESSION2] +
EXPRESSION2 :== TERMINAL | '!' EXPRESSION2 | CONVERSION '->' EXPRESSION2 |'(' EXPRESSION ')'
TERMINAL ::= TEST | REGEX_MATCHER
TEST ::= registered word
CONVERSION ::= registered conversion
REGEX_MATCHER ::= registered regex word '~' '/' regular expression '/'

Regex matchers can check the names against regular expressions.

The registered words, regex matchers, and conversions are numerous and documented in the following sections. The class Select allows you to register your tests, regex matchers, and conversions.

Annotated Elements Selectors

You can use the selectors annotation, and annotated to select any member or class annotated.

annotation ~ /regex/ is true if the examined member has an annotation that matches the regular expression.

annotated is true if the examined member has any annotation.

Class and method checking selectors

These conditions work on classes and on methods. Applying them on a field will throw an IllegalArgumentException exception.

  • abstract is true if the type of method or class is abstract.

  • implements is true if the class implements at least one interface. When applied to a method, it is true if it implements a method of the same name and argument types in one of the interfaces the class directly or indirectly implements. In other words, it means that there is an interface that declares this method, and this method is an implementation (not abstract).

Class checking selectors

These conditions can be applied to classes. That is because their meaning cannot be interpreted in the case of a method or field. For example, a field or method cannot be primitive or anonymous. However, to provide a shortcut, when these conditions are applied to methods and fields, their type is used. That way, for example, primitive applied to a field is the same as the expression type -> primitive. Similarly, primitive applied to a method is the same as returnType -> primitive.

If there is an alternative way to check the field or method type, you can use the one explicitly provided for fields and methods. For example, the returns applied to a method is the same as canonicalName. You can also choose to write the type -> primitive and returnType -> primitive conversions explicitly. You should choose the one that makes your heart happy. Just be consistent for the sake of the maintainers of your code.

When the documentation here says "…​ the type is …​" it means that the class or interface itself or the type of the field or the return type of the method if the condition is checked against a field or method respectively.

  • interface is true if the type is an interface

  • primitive is true when the type is a primitive type, a.k.a. int, double, char and so on. Note that String is not a primitive type.

  • annotation is true if the type is an annotation interface.

  • anonymous is true if the type is anonymous.

  • array is true if the type is an array.

  • enum is true if the type is an enumeration.

  • member is true if the type is a member class, a.k.a. inner or nested class or interface

  • local is true if the type is a local class. Local classes are defined inside a method.

  • extends without regular expression checks that the class explicitly extends some other class. (Implicitly extending Object does not count.)

  • extends ~ /regex/ is true if the canonical name of the superclass matches the regular expression. In other words, if the class extends the class given in the regular expression directly.

  • simpleName ~ /regex/ is true if the simple name of the class (the name without the package) matches the regular expression.

  • canonicalName ~ /regex/ is true if the canonical name of the class matches the regular expression.

  • name ~ /regex/ is true if the name of the class matches the regular expression. Note that fields and methods also have names. If you check the name against a method or a field, then the method’s or the field’s name is checked and not the name of the type. If you want to check the name of a method’s return type or a field’s type, you have to use the explicit conversion type or returnType with the operator ->.

  • implements ~ /regex/ is true if the type directly implements an interface whose name matches the regular expression. It is when the interface is directly listed following the implements keyword in the class declaration. You can also use implements without a regular expression. In that case, the meaning is slightly different and has a special meaning for methods.

Method checking selectors

These conditions work on methods. If applied to anything other than a method, the checking will throw an exception.

  • synthetic is true if the method is synthetic. The Java compiler generates synthetic methods in some particular situations. These methods do not appear in the source code.

  • synchronized is true if the method is synchronized.

  • native is true if the method is native.

  • strict is true if the method has the strict modifier. It was a rarely used modifier and affected the floating-point calculation. This keyword was introduced in Java 1.2 and was removed in Java 17. You can still use this modifier in Java 17 and later, but it has no effect.

  • default is true if the method is defined as a default method in an interface.

  • bridge is true if the method is a bridge method. The Java compiler generates bridge methods in some special situations. These methods do not appear in the source code.

  • vararg is true if the method is a variable argument method.

  • overrides is true if the method overrides another method in the superclass chain. Implementing a method declared in an interface alone will not result in true, even though methods implementing an interface method are annotated using the compile-time @Override annotation. The @Override annotation may or may not be used on the method. The result of the condition overrides does not depend on the presence of the @Override annotation. You can also note that you cannot check the use of the @Override annotation using reflection because this annotation is not visible at run-time.

  • void is true if the method has no return value.

  • returns ~ /regex/ is true if the method return type’s canonical name matches the regular expression. Note that this condition is almost the same as canonicalName. When applied to a method, then these two conditions are identical. The only difference is that while canonicalName works for classes and fields, returns will throw an exception if applied to anything but a method.

  • throws ~ /regex/ is true if the method throws a declared exception that matches the regular expression.

  • signature ~ /regex/ checks that the method’s signature matches the regular expression. The library reads the method description and creates the signature string to perform this check. When creating this string, the names of the arguments as provided in the source code are not available. Instead of the actual names, the library uses arg0, arg1, …​, argN. A comma and a single space separate the arguments. The types are expressed with all the generic parameters. The classes are expressed with canonical names, except those from the package java.lang. Types, like Integer, String, and so on, are represented using simple names. Varargs are represented using the …​ notation. The …​ follows the type name, and there is a single space before the argument’s name (argX).

Field checking selectors

These conditions work on fields. If applied to anything other than a field, the checking will throw an exception.

  • transient is true if the field is transient.

  • volatile is true if the field is declared volatile.

Universal selectors

These conditions work on fields, on classes, and methods.

  • true is always true.

  • false is always false.

  • null is true when the method, field, or class object is null. You can use it to test that a field, class, or method has a parent, enclosing class, or something else that we can examine with a -> operator. For example, the following line from the unit tests checks that the class Object has no parent.

    assertTrue(Selector.compile("superClass -> null").match(Object.class));

    null and true are the only conditions that return true for a null object. All other conditions will return false. The following line is a unit test that checks that the superclass of Object — which is null — is not an interface.

    assertFalse(Selector.compile("superClass -> interface").match(Object.class));
  • private is true if the examined member has private protection. If the checked object is a class, it eventually has to be an inner class.

  • protected is true if the examined member is protected.

  • package is true if the examined member has package-private protection.

  • public is true if the examined member is public.

  • static is true if the examined member is static.

  • final is true if the examined member is final.

  • class is true if the examined member is a class. It will result in false for null, a method, a field, or a class object, which is an interface. This condition is similar to !interface but differs in exceptional cases.

  • name ~ /regex/ is true if the examined member’s name matches the regular expression.

Conversion

Conversions are used to direct the next part of the expression to check something else instead of the member. Conversions are on the same level as the ! negation operator. The name of the conversion is separated from the following part of the expression by ->.

  • declaringClass check the declaring class instead of the member. You can apply it to methods, fields, and classes. Note that there is an enclosingClass, which you can apply to classes.