Modelling APIs with Coko
To evaluate rules for an API, Coko needs an understanding of the components of the API, such as classes or functions. In Coko these components are modelled through interfaces, classes and a class called Op
.
Ops¶
Ops
are the basic building blocks for writing policies in Coko. With Ops
you can model and group functions of the API that serve a similar functionality. They are also a way to define queries to the Codyze backend for finding calls to these functions. Each Op
object is one query to the backend.
There are currently two types of Ops
, FunctionOps
for modelling functions and ConstructorOps
for modelling constructors in object-oriented languages. They are both built with type-safe builders. The following sections will explain the builders for each Op
type.
FunctionOps¶
The function op()
is the start for building FunctionOps
. Within the block of op()
the fully qualified name of the functions you want to model can be specified as a string. In the block of the fully qualified name the arguments to function can be defined. They serve as a filter for the query. Only calls to the function with the same number of arguments with the same value at each position will be found by the query.
In signature
it is also possible to specify unordered arguments. These are arguments, that should somehow be passed to the function calls we want to find, but it is not important, in which position they are passed.
op {
"my.fully.qualified.name" {
signature(5..10)
signature(".*one.*")
signature(0..5, listOf("one", "two"))
signature()
}
"my.other.fully.qualified.name" {
signature {
- ".*"
-7
}
signature(arrayOf(4)) {
- 123
}
}
}
ConstructorOps¶
The function of the builder for ConstructorOps
is constructor()
. The fully qualified name of the class is the first argument. In the block of constructor()
you can specify the arguments to the constructor like for the FunctionOp
. They serve the same purpose as for FunctionOps
.
Special argument types¶
Arguments in Ops
are used to filter calls of a function based on the arguments that are given to the calls. In Coko there are a few special argument types that help with the filtering.
The first type is the Wildcard
object. If the Wildcard
object is given as an argument to Op
, the filter will allow any kind of value in the same argument position of the function calls.
The second type is null
. It can be used to signify that the given arguments should be filtered out of the query. This type will be helpful when constructing Op
templates with Coko functions.
Another special type are ParameterGroups
. They are a filter to express that the argument at the position should be composed of multiple values. An example would be if the argument is a string that should contain multiple strings, for example foo("Start string" + 12 + "End string")
. This can be modeled with ParameterGroups
. Coko offers the DSL function group
, in which these values can be specified.
The last types are the Type
class and the ParamWithType
class. The Type
class is used to filter the type of the arguments. The fully qualified name of the type must be given ParamWithType
combines the filter for the value with the filter for the type.
op {
"my.fully.qualified.name" {
signature(Wildcard)
signature(Wildcard, 2)
signature(null, 1)
signature(
group {
- "Start string .*"
- 12
}
)
signature(Type("java.util.List"))
signature(1.0 withType "java.lang.Float")
}
}
Functions¶
Since each Op
is interpreted by Codyze as one query for function calls to the backend, it would be helpful to have templates for Ops
that find calls to the same function but with different filters. This can be achieved with functions in Coko. The parameters of these functions in Coko can be used to pass the specific values to the filter.
fun add(
element: Any,
index: Any?
) = op {
"java.util.List.add" {
signature(element)
signature(index withType "int", element)
}
}
If the reference to a Coko function is used for order
rules, all parameters must have a nullable type. Coko invokes them with dummy arguments and uses internal functions to query for all calls to the modelled function regardless of the specified signatures.
Interfaces¶
A goal of Coko is to make rules more reusable. Aside from specific rules of an API, there might be some general policies that should be followed for all API that provide similar functionality. An example would be that all actions executed on a database should be logged. Instead of writing the corresponding rule for all combinations of database and logging APIs, one might want to combine this into one reusable rule.
This reusability is achieved through interfaces. In Coko interfaces and their functions describe the functionalities that a group of APIs has in common. Rules can thus be written on a more conceptual level.
When Coko encounters rules using a Coko interface as a parameter, it will use one of the available classes implementing the interface as argument to evaluate the rule. Currently, it uses the first class it finds, however, we will implement an algorithm in the future which will try to find the implementation that fits the best for the analyzed code.
interface Logging {
fun log(message: Any?, vararg args: Any?): Op
}
class JavaLogging: Logging {
override fun log(message: Any?, vararg args: Any?): Op =
op {
"java.util.logging.Logger.info" {
signature {
group {
- message
args.forEach { - it }
}
}
}
}
}
class PythonLogging: Logging {
override fun log(message: Any?, vararg args: Any?): Op =
op {
"logging.info" {
signature(args) {
- message
}
}
}
}
Classes¶
Classes in Coko model the actual components of an API. They can implement interfaces or just be normal classes. With classes, API functions can be grouped
For APIs written in object-oriented languages it might be sensible to create a Coko class for each class in the API.