The abide framework was build to be easily extensible, and not only with respect to rule addition. One can also extend two other key features of the framework, however doing so is slightly more involved as rule contribution.
Directives are simply traits that will be mixed in to the rule context at rule instantiation. By default, a simple
Context
value will be used, but this behavior can be overriden by defining a companion object to the rule that
extends the ContextGenerator
trait. These generators will instantiate the correct context for the companion rule and the abide compiler plugin will generalise the context in order to share cache.
Concretely, given a generator that creates Context
typed contexts and another that generates C <: Context
, all rules will share the context instance with type C
since it generalises Context
. This enables cache sharing without requiring knowledge of existing rules and context types.
To bind the the type C <: Context
to the rule that will be consuming this context, we define a companion object
object SomeRule extends ContextGenerator {
// C is typically a mixin of Context and some other stuff
def getContext(universe : SymbolTable) : C = new C
}
class SomeRule(val context : C) extends Rule { ... }
and the abide compiler plugin will automatically share the C
typed context between all rules for which it remains
valid.
Note that any SomeRule
subtype will also require a C
typed context even if SomeRule
is not actually used in the
verification run, and the framework will indeed instantiate a C
context to automatically guarantee type safety.
In order to perform rule verification, abide relies on Analyzer
instances that know how to actually apply a rule to source trees. For example, in the case of TraversalRule
classes, the FusingTraversalAnalyzer
will fuse the rules together before applying traversal to increase performance. This lets the analyzer optimize for global information that isn't available inside a particular rule.
When declaring a rule, an analyzer type has to be attached to it so the framework will know how to actually apply the rule. This is managed by the val analyzer : AnalyzerGenerator
field contained in rules. An AnalyzerGenerator
is an object that knows how to instantiate an Analyzer
given a Context
and a set of rules (the rules that share this generator object).
Since the analyzer attached to a rule is statically determined by the analyzer
field, we need a mechanism to inject new (and possibly more powerful) analyzers. For example, say we have the generator
object SomeAnalyzerGenerator extends AnalyzerGenerator {
def generateAnalyzer(universe : SymbolTable, rules : List[Rule]) : Analyzer =
new SomeAnalyzer(universe, rules)
}
and someone comes along with a new analyzer that should replace SomeAnalyzer
, but doesn't want to change the rule definitions (where the analyzer
field is specified) for some reason. It suffices to declare in the new generator that it subsumes SomeAnalyzerGenerator
.
object SomeOtherAnalyzerGenerator extends AnalyzerGenerator {
def generateAnalyzer(universe : SymbolTable, rules : List[Rule]) : Analyzer =
new SomeOtherAnalyzer(universe, rules)
val subsumes : Set[AnalyzerGenerator] = Set(SomeAnalyzerGenerator)
}
Any rule whose generator is transitively subsumed by another generator will be assigned to the analyzer generated by the later. This enables users to extend the framework with only analyzer plugins that will be automatically used to perform analysis by the compiler plugin.
In order to specify to the framework which generators are provided by a package, analyzer generator classes must be appended to the abide-plugin.xml descriptor in an <analyzer class="some.analyzer.generator.Class" />
element.
For a concrete example, see NaiveTraversalAnalyzer which is subsumed by FusingTraversalAnalyzer for traversal rules.
As of now, only the analyzer for unit-local, flow-agnostic rules has been written, but cross-unit and flow-sensitive backends should be added in the future (if deemed useful). However, many simple(r) rules do not actually need flow information and warnings can be collected by a single pass through a compilation unit's body. Such rules are called traversal rules in the abide lingo. See writing traversal rules and abide rules for more details.
In order to output results, abide relies on Presenter
instances that know how to actually process the output of abide.
For example, the ConsolePresenter
will output the results as compiler messages.
When declaring a presenter, a generator type has to be attached to it so the framework will know how to actually create the presenter.
A PresenterGenerator
is an object that knows how to instantiate an Presenter
given all the needed context and for it to run.
object SomePresenterGenerator extends PresenterGenerator {
def generatePresenter(global: Global) : Presenter =
new SomePresenter(global)
}
class SomePresenter(protected val global: Global) extends Presenter {
import global._
def apply(unit: CompilationUnit, warnings: List[Warning]): Unit = {
// TODO: implement the presenter
}
}