Implementing a New Metric
Relevant source files
The following files were used as context for generating this wiki page:
Implementing a new metric in rust-code-analysis involves defining a data structure to hold statistical results, implementing a specific trait for metric computation, and integrating the metric into the library's space-based analysis traversal. The library leverages Tree Sitter to parse source code into an Abstract Syntax Tree (AST), which is then traversed to calculate metrics at different levels of granularity, such as functions, classes, or units.
The implementation process generally requires defining how a metric is collected (e.g., counting specific AST nodes), how it is merged across different code spaces, and how it is finalized for output.
Sources: rust-code-analysis-book/src/developers/loc.md, src/spaces.rs:271-330
Core Architecture and Components
The metrics system is built around several key components that manage data storage, computation logic, and space-based aggregation.
1. Metric Stats Structure
Every metric must define a Stats struct. This struct holds the raw values (e.g., counters or accumulators) for a specific metric. It typically implements Default, Clone, Debug, and Serialize for output generation. For aggregate metrics that represent a suite (like Loc), the Stats struct might compose several sub-structures.
Sources: src/metrics/loc.rs:164-173, src/metrics/nom.rs:11-21
2. The Metric Trait
A trait must be defined for the metric (e.g., Loc, Nom, Mi). This trait contains a compute method that defines the logic for extracting information from an AST Node.
pub trait Nom
where
Self: Checker,
{
fn compute(node: &Node, stats: &mut Stats) {
if Self::is_func(node) {
stats.functions += 1;
return;
}
if Self::is_closure(node) {
stats.closures += 1;
}
}
}
Sources: src/metrics/nom.rs:154-165, src/metrics/loc.rs:324-328
3. Space Aggregation
Metrics are calculated within a FuncSpace. As the AST traversal moves from leaves to the root, metrics from subspaces are merged into their parent spaces.
The diagram shows the lifecycle of metric computation during AST traversal, highlighting how nodes trigger computations and boundaries trigger space management.
Sources: src/spaces.rs:271-315
Implementation Steps
Defining the Stats and Merging Logic
The Stats struct must implement a merge method. This is critical for the "bottom-up" aggregation where metrics from functions are rolled up into classes or files.
| Method | Purpose |
|---|---|
merge(&mut self, other: &Stats) | Combines stats from a subspace into the current space. |
compute_sum(&mut self) | Accumulates internal counters into a final sum field. |
compute_minmax(&mut self) | Updates minimum and maximum recorded values for the metric. |
Sources: src/metrics/loc.rs:222-234, src/metrics/nom.rs:90-98
Implementing Language-Specific Logic
Because each language has a different grammar, the metric trait is implemented for specific language code types (e.g., PythonCode, RustCode). This often involves matching the kind_id() of a node against specific language enums.
impl Loc for PythonCode {
fn compute(node: &Node, stats: &mut Stats, is_func_space: bool, is_unit: bool) {
use Python::*;
match node.kind_id().into() {
Comment => { add_cloc_lines(stats, start, end); }
IfStatement | ForStatement | WhileStatement => {
stats.lloc.logical_lines += 1;
}
// ... other mappings
}
}
}
Sources: src/metrics/loc.rs:372-421, src/metrics/npm.rs:136-168
Integration with CodeMetrics
Once a metric is implemented, it must be added to the CodeMetrics struct in src/spaces.rs. This struct acts as a container for all metrics computed for a single FuncSpace.
Metric Registration Lifecycle
- Add to
CodeMetrics: Include the newStatsstruct as a field inCodeMetrics. - Update
merge: UpdateCodeMetrics::mergeto call the new metric's merge logic. - Update Traversal: In the
metricsfunction, call the new metric'scomputemethod for every node encountered during traversal. - Update Output: Add the metric to
dump_metrics.rsto ensure it appears in CLI output.
The sequence diagram illustrates how the metrics() function orchestrates calls to various metric traits and manages the finalization of states.
Sources: src/spaces.rs:271-330, src/output/dump_metrics.rs:104-120
Finalization and Output
After traversal, metrics often require a "finalization" step to calculate averages or derived values (like the Maintainability Index).
Derived Metrics (MI and WMC)
Some metrics depend on others. For example, the Mi (Maintainability Index) requires Loc, Cyclomatic, and Halstead stats to be computed first. These are finalized in compute_halstead_mi_and_wmc.
pub fn mi_visual_studio(&self) -> f64 {
let formula = 171.0
- 5.2 * self.halstead_volume.ln()
- 0.23 * self.cyclomatic
- 16.2 * self.sloc.ln();
(formula * 100.0 / 171.0).max(0.)
}
Sources: src/metrics/mi.rs:77-84, src/spaces.rs:232-246
Output Formatting
The dump_metrics.rs file contains functions to print metrics to the console. Each new metric should implement a dump_[metric_name] function using the termcolor crate to maintain consistent styling.
Sources: src/output/dump_metrics.rs:200-220
Conclusion
Implementing a new metric requires a modular approach: defining the data container, the computation logic via traits, and the aggregation logic for nested code spaces. By adhering to the Stats and merge patterns, new metrics automatically benefit from the library's multi-language support and space-aware analysis.
Sources: src/spaces.rs:271-315, rust-code-analysis-book/src/developers/loc.md