Coko Rules
Rules in Coko describe how an API should be used. They are functions that are annotated with the @Rule
annotation.
In the @Rule
annotation you can specify metadata about the rule such as the description of the rule. The metadata will be used for describing the findings in the SARIF output.
If the rule requires some instance of a model, they can be specified as parameters to the rule function.
Each Coko rule must return an implementation of the Evaluator
interface, which Codyze can use to evaluate the rule. Coko provides some common evaluators which will be explained in the following sections. The example model will be used for explaining the evaluators.
class Foo {
fun constructor() = constructor("Foo") {
signature()
}
fun first(i: Any?) = op {
definition("Foo.first") {
signature(i)
}
}
fun second(s: Any?) = op {
definition("Foo.second") {
signature(s)
}
}
}
class Bar {
fun second() = op {
definition("Bar.second") {
signature()
}
}
}
Only Evaluator¶
The only
evaluator checks if all calls to an Op
are only called with the specified arguments. Therefore, it takes one Op
as argument.
@Rule
fun `only calls to first with 1 allowed`(foo: Foo) =
only(foo.first(1))
Order Evaluator¶
The order
evaluator checks if functions related to an object are called in the correct order. It takes two arguments, the baseNodes
and the order. The baseNodes
are the function calls that are the start of the order. Usually, this is either the constructor of a class or some kind of initialization function.
To construct the order, Coko provides a type-safe builder. Within the builder, the order is specified as a regular expression.
The "alphabet" of the order regex is:
- references to functions that return an
Op
written as<object>::<FunctionName>
or::<FunctionName>
Ops
themselves.
If all calls to a modelled function should be considered for the order regardless of the specified signatures, please use the first option. When passing Ops
, only functions that match the used signature and argument are considered valid.
The builder provides a set of functions that allow you to add quantifiers to the regex or group them.
Function | Regex | Description |
---|---|---|
or | | | Represents a choice, either the first or the second expression has to be matched |
set | [] | Represents multiple choices, one expression in it has to be matched |
maybe | * | Matches an expression zero or more times |
some | + | Matches an expression one or more times |
option | ? | Matches an expression zero or one time |
count(n) | {n } | Matches an expression exactly n times |
atLeast(min) | {min ,} | Matches an expression at least min times |
between(min, max) | {min , max } | Matches an expression at least min and at most max times |
@Rule
fun `order of Foo`(foo: Foo) =
order(foo.constructor()/* (2)! */) { // (1)!
- foo.first(...) // (3)!
maybe(foo::second) // (4)!
}
- This starts the type-safe builder for the order.
- The
Op
returned fromfoo.constructor
will be used as query for the function calls that are the starting point for evaluating the order. - This will use the filtered
Op
returned byfoo.first(...)
for the order. - This will consider all calls to the function modelled by
foo.second()
for the order. No filter will be applied.
FollowedBy Evaluator¶
The followedBy
evaluator works similarly like the implication in logic. It takes two Ops
and specifies that if the first Op
is called then the second Op
must be called as well. Compared to the order
evaluator, followedBy
is more flexible because Ops
from different models can be connected.
@Rule
fun `if first then second`(foo: Foo, bar: Bar) =
foo.first(Wildcard) followedBy bar.second()
Precedes Evaluator¶
The precedes
evaluator is the logical counterpart to the followedBy
evaluator, implementing a logical reverse implication. Similar to the previous evaluator, it takes two Ops
. Whenever the second Op
is called, the first Op
must have been called before. In contrast to the followedBy
evaluator, the second Op
acts as the trigger for the rule and no finding is generated when only the first Op
is encountered in any given context.
@Rule
fun `always first before second`(foo: Foo, bar: Bar) =
foo.first(Wildcard) precedes bar.second()
Never Evaluator¶
The never
evaluator is used to specify that calls to an Op
with the specified arguments are forbidden. It takes one Op
as argument.
Argument Evaluator¶
The argumentOrigin
evaluator is used to trace back the argument of a call to a specific method call. It takes three arguments: - The target Op
whose argument we want to verify - The position of the argument in question (0-based indexing) - The origin Op
which should have produced the argument
The evaluator will then try to check whether the argument of the target Op
was always produced by a call to the origin Op
. If this is not the case or the Evaluator lacks information to clearly determine the origin of the argument, it will generate a finding.