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.
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.
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.
These conditions work on classes and on methods.
Applying them on a field will throw an IllegalArgumentException
exception.
-
abstract
istrue
if the type of method or class is abstract. -
implements
istrue
if the class implements at least one interface. When applied to a method, it istrue
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).
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
istrue
if the type is an interface -
primitive
istrue
when the type is a primitive type, a.k.a.int
,double
,char
and so on. Note thatString
is not a primitive type. -
annotation
istrue
if the type is an annotation interface. -
anonymous
istrue
if the type is anonymous. -
array
istrue
if the type is an array. -
enum
istrue
if the type is an enumeration. -
member
istrue
if the type is a member class, a.k.a. inner or nested class or interface -
local
istrue
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 extendingObject
does not count.) -
extends ~ /regex/
istrue
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/
istrue
if the simple name of the class (the name without the package) matches the regular expression. -
canonicalName ~ /regex/
istrue
if the canonical name of the class matches the regular expression. -
name ~ /regex/
istrue
if the name of the class matches the regular expression. Note that fields and methods also have names. If you check thename
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 conversiontype
orreturnType
with the operator->
. -
implements ~ /regex/
istrue
if the type directly implements an interface whose name matches the regular expression. It is when the interface is directly listed following theimplements
keyword in the class declaration. You can also useimplements
without a regular expression. In that case, the meaning is slightly different and has a special meaning for methods.
These conditions work on methods. If applied to anything other than a method, the checking will throw an exception.
-
synthetic
istrue
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
istrue
if the method is synchronized. -
native
istrue
if the method is native. -
strict
istrue
if the method has thestrict
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
istrue
if the method is defined as a default method in an interface. -
bridge
istrue
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
istrue
if the method is a variable argument method. -
overrides
istrue
if the method overrides another method in the superclass chain. Implementing a method declared in an interface alone will not result intrue
, 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 conditionoverrides
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
istrue
if the method has no return value. -
returns ~ /regex/
istrue
if the method return type’s canonical name matches the regular expression. Note that this condition is almost the same ascanonicalName
. When applied to a method, then these two conditions are identical. The only difference is that whilecanonicalName
works for classes and fields,returns
will throw an exception if applied to anything but a method. -
throws ~ /regex/
istrue
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 usesarg0
,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 packagejava.lang
. Types, likeInteger
,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
).
These conditions work on fields. If applied to anything other than a field, the checking will throw an exception.
-
transient
istrue
if the field is transient. -
volatile
istrue
if the field is declared volatile.
These conditions work on fields, on classes, and methods.
-
true
is alwaystrue
. -
false
is alwaysfalse
. -
null
istrue
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 classObject
has no parent.assertTrue(Selector.compile("superClass -> null").match(Object.class));
null
andtrue
are the only conditions that returntrue
for anull
object. All other conditions will returnfalse
. The following line is a unit test that checks that the superclass ofObject
— which isnull
— is not an interface.assertFalse(Selector.compile("superClass -> interface").match(Object.class));
-
private
istrue
if the examined member has private protection. If the checked object is a class, it eventually has to be an inner class. -
protected
istrue
if the examined member is protected. -
package
istrue
if the examined member has package-private protection. -
public
istrue
if the examined member is public. -
static
istrue
if the examined member is static. -
final
istrue
if the examined member is final. -
class
istrue
if the examined member is a class. It will result infalse
fornull
, 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/
istrue
if the examined member’s name matches the regular expression.
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 anenclosingClass
, which you can apply to classes.