Architecture¶
Module Dependency Graph¶
cli ──→ core
├──→ lang-java ──→ core
├──→ lang-groovy ──→ core
├──→ lang-kotlin ──→ core
├──→ lang-javascript ──→ core
└──→ reporting ──→ core
gradle-plugin ──→ core
├──→ lang-java
├──→ lang-groovy
├──→ lang-kotlin
└──→ lang-javascript
All language modules depend on core only. Language modules are independent of each other. The shared ANTLR Java grammar and parsing utilities (BlockAnalysis, MethodClassification) live in core so that lang-groovy and lang-kotlin can use them as fallback parsers without depending on lang-java.
Core Module¶
The core module defines all abstractions and shared utilities. It contains no language-specific parsing logic.
model/— IR node types (IRNode,LoopNode,LookupCall,SortCall,FunctionDecl, etc.),SourceLocation,Language,Severity,ExecutionContextrules/—Ruleinterface,Finding,Evidence,AnalysisContext, all built-in rule implementationsengine/—AnalysisEngine(four-pass orchestrator),LanguageParserinterface,ParserRegistry,SuppressionFiltersemantics/—LanguageSemanticsRegistry(loads and merges language + framework YAML files)graph/—SymbolTable,CallGraphconfig/—AnalysisConfigdata classcache/—AnalysisCachefor incremental analysisbaseline/—Baselinefor diff modelang/java/parser/—BlockAnalysisandMethodClassificationutilities, shared by all ANTLR-based language modules
ParserRegistry¶
ParserRegistry is a singleton object in core/engine/. Each language module registers its parser once from its parser class's companion object init {} block. The CLI and engine retrieve parsers from the registry — there is no hardcoded list of parsers in the engine itself.
Registration happens lazily: Kotlin only runs companion object init when the class is first loaded. The CLI touches each companion object explicitly to trigger loading before starting analysis.
Four-Pass Engine¶
- Parse & Collect — Each file is parsed into an IR tree by its language parser. Symbols are registered in
SymbolTable. - Call Graph Construction — Function calls are resolved to their declarations, building the
CallGraph. - Complexity Annotation — Execution contexts (
LOOP,CALLBACK,SORT_COMPARATOR) are propagated through the call graph to all reachable call sites. - Rule Evaluation — Each rule traverses the IR trees and produces
Findingobjects.
After rule evaluation, two post-processing steps run before findings reach the user:
- Subsumption — When two rules fire at the same source location and one is a more specific version of the other, the less specific finding is dropped.
- Suppression — Findings on lines with
// algorilla:ignorecomments are removed.
Rule Subsumption¶
Some rules detect overlapping patterns. For example, nested-lookup and expensive-callback can both fire on list.filter(x -> other.contains(x.id)) — one from the "linear search inside a loop" angle, the other from the "expensive operation inside a callback" angle. Reporting both is noise.
The subsumption model lets rules declare which other rules they make redundant via the subsumes field on the Rule interface (see NestedLookupRule.kt for an example).
The engine groups findings by source location (file + line). Within each group, if rule A fired and A declares subsumes = setOf("B"), any finding from rule B in that group is dropped. Rule B's findings at other locations are unaffected.
Current subsumption declarations:
| Dominant rule | Subsumes |
|---|---|
nested-lookup |
expensive-callback |
redundant-expensive-call |
uncached-getter |
When adding a new rule, check whether it overlaps with existing rules and add a subsumes declaration if appropriate. See Adding Rules.
Semantics and Framework Overlays¶
Language semantics live in YAML files under core/src/main/resources/semantics/. Each file defines method categories (expensive, cheap, stream operations, etc.) for one language.
Framework overlays follow the same YAML format and live under semantics/frameworks/. They are discovered automatically at runtime from semantics/frameworks-index.txt — adding a new framework only requires creating the YAML file and appending its filename to the index. Each framework YAML must include a language: key so the registry knows which language it extends.
AnalysisCache computes a version hash from all semantics and framework files. When any YAML changes, the hash changes and all cached results are invalidated automatically.
Extension Points¶
- New language — Implement
LanguageParser, self-register viacompanion object init, add a semantics YAML, trigger class loading from the CLI. See Adding Languages. - New rule — Implement
Rule, register inBuiltinRules.all(). See Adding Rules. - New framework overlay — Add a YAML file under
semantics/frameworks/and append its name toframeworks-index.txt. No code changes required. - New reporter — Implement the
Reporterinterface in thereportingmodule.