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
//! Budget lint pass (FA3xx).
//!
//! Detects:
//! - FA301: Unbounded allocations
//! - FA302: Missing budget guards
use crate::config::Config;
use crate::diagnostics::{Diagnostic, DiagnosticBuilder};
use crate::cli::Severity;
use crate::lints::{is_framealloc_call, FrameallocCall};
use crate::parser::span_to_location;
use std::path::Path;
use syn::visit::Visit;
use syn::spanned::Spanned;
pub fn check(ast: &syn::File, path: &Path, config: &Config) -> Vec<Diagnostic> {
let mut visitor = BudgetsVisitor::new(path, config);
visitor.visit_file(ast);
visitor.diagnostics
}
struct BudgetsVisitor<'a> {
path: &'a Path,
config: &'a Config,
diagnostics: Vec<Diagnostic>,
// Track loop allocations for unbounded detection
loop_alloc_count: usize,
in_loop: bool,
loop_span: Option<proc_macro2::Span>,
}
impl<'a> BudgetsVisitor<'a> {
fn new(path: &'a Path, config: &'a Config) -> Self {
Self {
path,
config,
diagnostics: Vec::new(),
loop_alloc_count: 0,
in_loop: false,
loop_span: None,
}
}
}
impl<'a> Visit<'a> for BudgetsVisitor<'a> {
fn visit_expr(&mut self, expr: &'a syn::Expr) {
match expr {
// Track loops
syn::Expr::Loop(loop_expr) => {
let was_in_loop = self.in_loop;
let prev_count = self.loop_alloc_count;
let prev_span = self.loop_span;
self.in_loop = true;
self.loop_alloc_count = 0;
self.loop_span = Some(loop_expr.span());
syn::visit::visit_expr_loop(self, loop_expr);
// Check if we exceeded threshold
self.check_loop_allocations();
self.in_loop = was_in_loop;
self.loop_alloc_count = prev_count;
self.loop_span = prev_span;
return;
}
syn::Expr::While(while_expr) => {
let was_in_loop = self.in_loop;
let prev_count = self.loop_alloc_count;
let prev_span = self.loop_span;
self.in_loop = true;
self.loop_alloc_count = 0;
self.loop_span = Some(while_expr.span());
syn::visit::visit_expr_while(self, while_expr);
self.check_loop_allocations();
self.in_loop = was_in_loop;
self.loop_alloc_count = prev_count;
self.loop_span = prev_span;
return;
}
syn::Expr::ForLoop(for_expr) => {
let was_in_loop = self.in_loop;
let prev_count = self.loop_alloc_count;
let prev_span = self.loop_span;
self.in_loop = true;
self.loop_alloc_count = 0;
self.loop_span = Some(for_expr.span());
syn::visit::visit_expr_for_loop(self, for_expr);
self.check_loop_allocations();
self.in_loop = was_in_loop;
self.loop_alloc_count = prev_count;
self.loop_span = prev_span;
return;
}
// Count allocations in loops
syn::Expr::MethodCall(_) => {
if let Some(fa_call) = is_framealloc_call(expr) {
if fa_call.is_any_allocation() && self.in_loop {
self.loop_alloc_count += 1;
}
}
}
_ => {}
}
syn::visit::visit_expr(self, expr);
}
}
impl<'a> BudgetsVisitor<'a> {
fn check_loop_allocations(&mut self) {
// This is a static heuristic - we can't know actual iteration count
// but we can warn about patterns that typically cause budget issues
if self.loop_alloc_count > 0 && self.config.is_lint_enabled("FA301") {
// For now, we just detect the pattern exists
// More sophisticated analysis would try to determine loop bounds
if let Some(span) = self.loop_span {
// Only warn if there are multiple allocation calls in the loop
// Single allocations might be intentional
if self.loop_alloc_count >= 2 {
self.diagnostics.push(
DiagnosticBuilder::new("FA301")
.severity(Severity::Hint)
.message(format!(
"loop contains {} allocation calls - verify budget constraints",
self.loop_alloc_count
))
.location(span_to_location(span, self.path))
.note("loops with allocations can exhaust memory budgets")
.suggestion("consider pre-allocating or using with_budget() guards")
.build()
);
}
}
}
}
}