1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
#![cfg_attr(coverage_nightly, coverage(off))]
//! Accurate Complexity Analyzer using AST-based analysis
//!
//! Sprint 63: Implements industry-standard complexity calculations
//! - Cyclomatic Complexity: Based on `McCabe` (1976) - decision points
//! - Cognitive Complexity: Based on `SonarSource` specification
//! - Supports test exclusion and annotation suppression
use anyhow::Result;
use std::path::Path;
use syn::{visit::Visit, Attribute, Expr, Item, ItemFn, Stmt};
use walkdir::WalkDir;
use crate::services::source_line_index::{FunctionSpans, LineSpan};
/// Accurate complexity analyzer with proper AST-based calculation
pub struct AccurateComplexityAnalyzer {
exclude_tests: bool,
respect_annotations: bool,
}
impl Default for AccurateComplexityAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl AccurateComplexityAnalyzer {
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
/// Create a new instance.
pub fn new() -> Self {
Self {
exclude_tests: false,
respect_annotations: false,
}
}
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
/// Exclude tests.
pub fn exclude_tests(mut self, exclude: bool) -> Self {
self.exclude_tests = exclude;
self
}
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
/// Respect annotations.
pub fn respect_annotations(mut self, respect: bool) -> Self {
self.respect_annotations = respect;
self
}
}
/// Result of analyzing a single file
#[derive(Debug, Clone)]
pub struct FileComplexityResult {
pub functions: Vec<FunctionMetrics>,
pub file_path: String,
/// Real number of lines in the file. Callers used to derive this from the
/// last function's invented `line_end`, which reported a 13-line file as
/// 61 lines (#652).
pub total_lines: u32,
}
/// Metrics for a single function
#[derive(Debug, Clone)]
pub struct FunctionMetrics {
pub name: String,
pub cyclomatic_complexity: u32,
pub cognitive_complexity: u32,
/// Deepest nesting level reached inside the body (measured, not derived
/// from cognitive complexity).
pub max_nesting: u32,
pub suppressed: bool,
/// 1-based line number where the function starts; 0 when not located.
pub line_start: u32,
/// 1-based inclusive line where the function ends; 0 when not located.
pub line_end: u32,
}
/// Cyclomatic/cognitive/nesting for one function body.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct BlockComplexity {
pub cyclomatic: u32,
pub cognitive: u32,
pub max_nesting: u32,
}
/// Measure one function body with the visitor `analyze complexity` and
/// `quality-gate` use.
///
/// `pmat context` used to run a private "GREEN PHASE" stub that only inspected
/// top-level statements and set `cognitive = cyclomatic`; on the same function
/// it reported 3/3 where this visitor reports 9/15 (#686). Routing every caller
/// through here is what makes the commands agree.
#[must_use]
pub fn measure_block(function_name: &str, block: &syn::Block) -> BlockComplexity {
let mut visitor = ComplexityVisitor::new().with_function_name(function_name.to_string());
visitor.visit_block(block);
BlockComplexity {
cyclomatic: visitor.cyclomatic,
cognitive: visitor.cognitive,
max_nesting: visitor.max_nesting,
}
}
/// Result of analyzing a project
#[derive(Debug, Clone)]
pub struct ProjectComplexityResult {
pub files_analyzed: usize,
pub file_metrics: Vec<FileComplexityResult>,
}
/// Build a map of function name -> measured 1-based line span from source text.
///
/// Spans come from [`FunctionSpans`], which reads the real closing brace. The
/// previous version recorded only the start line, which forced callers to
/// invent an end (#652, #656).
fn build_function_line_map(content: &str) -> FunctionSpans {
FunctionSpans::from_source(content)
}
/// One function definition found in a Rust file, with the body to measure.
///
/// Borrowed from the parsed tree so discovery allocates nothing but the name.
pub struct DiscoveredFn<'a> {
/// Bare identifier, matching how [`FunctionSpans`] keys the source scan.
pub name: String,
/// Attributes on the definition (used for `#[allow(complex_function)]`).
pub attrs: &'a [Attribute],
/// The body whose decision points are counted.
pub block: &'a syn::Block,
}
/// Every function definition in `items`, in source order.
///
/// This is the single answer to "which functions does a Rust file contain".
/// The complexity walker used to ask only for top-level `Item::Fn`, so a file
/// whose functions all lived inside `impl` / `trait` / `mod` blocks — i.e. most
/// idiomatic Rust — measured as *zero* functions with `max_cyclomatic: 0`, and
/// any gate reading those numbers passed the file unconditionally. 50 bodies as
/// free functions scored `total_functions: 50`; the same 50 bodies moved into
/// `impl S { .. }` scored 0.
///
/// Source order matters: spans are consumed from [`FunctionSpans`] in textual
/// order, so two functions sharing a name must be visited in the order they
/// appear in the text.
#[must_use]
pub fn collect_functions(items: &[Item]) -> Vec<DiscoveredFn<'_>> {
let mut found = Vec::new();
collect_functions_into(items, &mut found);
found
}
fn collect_functions_into<'a>(items: &'a [Item], found: &mut Vec<DiscoveredFn<'a>>) {
for item in items {
match item {
Item::Fn(func) => found.push(DiscoveredFn {
name: func.sig.ident.to_string(),
attrs: &func.attrs,
block: &func.block,
}),
Item::Impl(block) => {
for member in &block.items {
if let syn::ImplItem::Fn(func) = member {
found.push(DiscoveredFn {
name: func.sig.ident.to_string(),
attrs: &func.attrs,
block: &func.block,
});
}
}
}
Item::Trait(decl) => {
for member in &decl.items {
if let syn::TraitItem::Fn(func) = member {
// A required method (`fn f(&self);`) has no body: there
// is no code to measure, so it is not counted. Only
// default methods carry a block.
if let Some(block) = &func.default {
found.push(DiscoveredFn {
name: func.sig.ident.to_string(),
attrs: &func.attrs,
block,
});
}
}
}
}
// `mod foo;` has no inline body; `mod foo { .. }` does.
Item::Mod(module) => {
if let Some((_, nested)) = &module.content {
collect_functions_into(nested, found);
}
}
_ => {}
}
}
}
// --- Submodule includes ---
include!("accurate_complexity_analyzer_core.rs");
include!("accurate_complexity_analyzer_visitor.rs");
include!("accurate_complexity_analyzer_tests.rs");