use crate::cli::Severity;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Diagnostic {
pub code: DiagnosticCode,
pub severity: Severity,
pub message: String,
pub location: Location,
pub notes: Vec<String>,
pub suggestion: Option<Suggestion>,
pub related: Vec<RelatedLocation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Location {
pub file: PathBuf,
pub line: usize,
pub column: usize,
pub end_line: Option<usize>,
pub end_column: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelatedLocation {
pub location: Location,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Suggestion {
pub message: String,
pub replacement: Option<String>,
pub applicability: Applicability,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum Applicability {
MachineApplicable,
MaybeIncorrect,
HasPlaceholders,
Unspecified,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiagnosticCode {
pub code: String,
pub category: Category,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum Category {
Lifetime,
AsyncSafety,
Architecture,
Threading,
Budgets,
}
impl DiagnosticCode {
pub fn new(code: &str) -> Self {
let category = match &code[2..3] {
"6" => Category::Lifetime,
"7" => Category::AsyncSafety,
"8" => Category::Architecture,
"2" => Category::Threading,
"3" => Category::Budgets,
_ => Category::Lifetime,
};
Self {
code: code.to_string(),
category,
}
}
}
impl std::fmt::Display for DiagnosticCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.code)
}
}
pub fn fa201(location: Location, context: &str) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA201"),
severity: Severity::Error,
message: "frame allocation used across thread boundary without explicit transfer".to_string(),
location,
notes: vec![
format!("detected in context: {}", context),
"frame allocations are thread-local and cannot be safely shared".to_string(),
],
suggestion: Some(Suggestion {
message: "use frame_box_for_transfer() for explicit cross-thread handoff".to_string(),
replacement: None,
applicability: Applicability::MaybeIncorrect,
}),
related: vec![],
}
}
pub fn fa202(location: Location) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA202"),
severity: Severity::Warning,
message: "thread not registered with FrameBarrier but shares frame boundary".to_string(),
location,
notes: vec![
"threads sharing frame boundaries should be synchronized via FrameBarrier".to_string(),
],
suggestion: Some(Suggestion {
message: "register thread with FrameBarrier or ensure proper synchronization".to_string(),
replacement: None,
applicability: Applicability::HasPlaceholders,
}),
related: vec![],
}
}
pub fn fa203(location: Location) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA203"),
severity: Severity::Hint,
message: "thread performs allocations without explicit budget configuration".to_string(),
location,
notes: vec![
"explicit budgets help prevent unexpected memory growth".to_string(),
"consider setting per-thread frame budgets".to_string(),
],
suggestion: Some(Suggestion {
message: "configure thread budget with ThreadBudgetManager".to_string(),
replacement: None,
applicability: Applicability::Unspecified,
}),
related: vec![],
}
}
pub fn fa204(location: Location, pattern: &str) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA204"),
severity: Severity::Warning,
message: "pattern may cause deferred free queue overflow".to_string(),
location,
notes: vec![
format!("detected pattern: {}", pattern),
"unbounded cross-thread frees can cause memory pressure".to_string(),
],
suggestion: Some(Suggestion {
message: "configure bounded deferred queue with DeferredConfig::bounded()".to_string(),
replacement: None,
applicability: Applicability::HasPlaceholders,
}),
related: vec![],
}
}
pub fn fa205(location: Location, barrier_location: Option<Location>) -> Diagnostic {
let mut diag = Diagnostic {
code: DiagnosticCode::new("FA205"),
severity: Severity::Error,
message: "end_frame() called without barrier synchronization in multi-threaded context".to_string(),
location,
notes: vec![
"concurrent end_frame() calls can cause undefined behavior".to_string(),
"use FrameBarrier to synchronize frame boundaries".to_string(),
],
suggestion: Some(Suggestion {
message: "coordinate frame boundaries with FrameBarrier::wait_all()".to_string(),
replacement: None,
applicability: Applicability::HasPlaceholders,
}),
related: vec![],
};
if let Some(barrier_loc) = barrier_location {
diag.related.push(RelatedLocation {
location: barrier_loc,
message: "barrier defined here".to_string(),
});
}
diag
}
pub fn fa601(location: Location, escaped_to: &str) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA601"),
severity: Severity::Warning,
message: "frame allocation may escape frame scope".to_string(),
location,
notes: vec![
format!("allocation appears to be stored in: {}", escaped_to),
"frame allocations are invalidated at end_frame()".to_string(),
],
suggestion: Some(Suggestion {
message: "consider using pool_box() or heap_box() for data that outlives the frame".to_string(),
replacement: None,
applicability: Applicability::HasPlaceholders,
}),
related: vec![],
}
}
pub fn fa602(location: Location, alloc_type: &str, loop_type: &str) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA602"),
severity: Severity::Warning,
message: format!("{} allocation inside {} loop", alloc_type, loop_type),
location,
notes: vec![
"allocations in tight loops can cause performance issues".to_string(),
"consider pre-allocating or using frame_vec()".to_string(),
],
suggestion: Some(Suggestion {
message: "move allocation outside loop or use a pre-allocated buffer".to_string(),
replacement: None,
applicability: Applicability::HasPlaceholders,
}),
related: vec![],
}
}
pub fn fa603(location: Location) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA603"),
severity: Severity::Warning,
message: "frame-structured loop without frame lifecycle calls".to_string(),
location,
notes: vec![
"detected a main loop pattern without begin_frame()/end_frame()".to_string(),
"frame allocations may accumulate indefinitely".to_string(),
],
suggestion: Some(Suggestion {
message: "add alloc.begin_frame() at loop start and alloc.end_frame() at loop end".to_string(),
replacement: None,
applicability: Applicability::HasPlaceholders,
}),
related: vec![],
}
}
pub fn fa604(location: Location, policy: &str, actual_usage: &str) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA604"),
severity: Severity::Hint,
message: format!("retention policy '{}' may not match usage pattern", policy),
location,
notes: vec![
format!("observed usage: {}", actual_usage),
],
suggestion: Some(Suggestion {
message: "review retention policy choice".to_string(),
replacement: None,
applicability: Applicability::Unspecified,
}),
related: vec![],
}
}
pub fn fa605(location: Location) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA605"),
severity: Severity::Warning,
message: "allocation with Discard policy stored beyond frame scope".to_string(),
location,
notes: vec![
"RetentionPolicy::Discard means data is lost at frame end".to_string(),
"but this allocation appears to be stored in a persistent structure".to_string(),
],
suggestion: Some(Suggestion {
message: "use PromoteToPool or PromoteToHeap if data needs to persist".to_string(),
replacement: None,
applicability: Applicability::HasPlaceholders,
}),
related: vec![],
}
}
pub fn fa701(location: Location) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA701"),
severity: Severity::Error,
message: "frame allocation in async function".to_string(),
location,
notes: vec![
"async functions may suspend across frame boundaries".to_string(),
"frame allocations become invalid after end_frame()".to_string(),
],
suggestion: Some(Suggestion {
message: "use pool_box() or heap_box() for data in async contexts".to_string(),
replacement: None,
applicability: Applicability::MaybeIncorrect,
}),
related: vec![],
}
}
pub fn fa702(location: Location, await_location: Location) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA702"),
severity: Severity::Error,
message: "frame allocation used across await point".to_string(),
location,
notes: vec![
"the allocation is created before an await".to_string(),
"and used after the await completes".to_string(),
"frames may have been reset during the await".to_string(),
],
suggestion: Some(Suggestion {
message: "complete frame work before awaiting, or use persistent allocation".to_string(),
replacement: None,
applicability: Applicability::HasPlaceholders,
}),
related: vec![RelatedLocation {
location: await_location,
message: "await point here".to_string(),
}],
}
}
pub fn fa703(location: Location, capture_type: &str) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA703"),
severity: Severity::Error,
message: format!("FrameBox captured by {}", capture_type),
location,
notes: vec![
format!("{} may outlive the current frame", capture_type),
"FrameBox becomes invalid after end_frame()".to_string(),
],
suggestion: Some(Suggestion {
message: "use PoolBox or HeapBox for data captured by closures/tasks".to_string(),
replacement: None,
applicability: Applicability::MaybeIncorrect,
}),
related: vec![],
}
}
pub fn fa801(location: Location, expected_tag: &str, actual_tag: &str, module: &str) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA801"),
severity: Severity::Warning,
message: format!("allocation tag '{}' unexpected in module '{}'", actual_tag, module),
location,
notes: vec![
format!("expected tags for this module: {}", expected_tag),
"tag mismatches may indicate architectural confusion".to_string(),
],
suggestion: Some(Suggestion {
message: format!("use tag '{}' or move allocation to appropriate module", expected_tag),
replacement: None,
applicability: Applicability::HasPlaceholders,
}),
related: vec![],
}
}
pub fn fa802(location: Location, tag: &str) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA802"),
severity: Severity::Hint,
message: format!("unknown allocation tag '{}'", tag),
location,
notes: vec![
"this tag is not in the known_tags list in .fa.toml".to_string(),
"consider adding it or using an existing tag".to_string(),
],
suggestion: None,
related: vec![],
}
}
pub fn fa803(location: Location, from_module: &str, to_module: &str) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA803"),
severity: Severity::Warning,
message: format!("allocation intent crosses module boundary: {} -> {}", from_module, to_module),
location,
notes: vec![
"allocations typically should stay within their module's concerns".to_string(),
],
suggestion: None,
related: vec![],
}
}
pub fn fa901(location: Location) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA901"),
severity: Severity::Warning,
message: "QueryFilter should be imported from rapier::pipeline, not rapier::geometry".to_string(),
location,
notes: vec![
"In Rapier 0.31, QueryFilter was moved from geometry to pipeline module".to_string(),
"Using the old import will cause compilation errors".to_string(),
],
suggestion: Some(Suggestion {
message: "change import to: use rapier2d::pipeline::QueryFilter".to_string(),
replacement: Some("rapier::pipeline".to_string()),
applicability: Applicability::MachineApplicable,
}),
related: vec![],
}
}
pub fn fa902(location: Location) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA902"),
severity: Severity::Warning,
message: "BroadPhase has been renamed to BroadPhaseBvh in Rapier 0.31".to_string(),
location,
notes: vec![
"The broad phase implementation changed in Rapier 0.31".to_string(),
"Update all references to use BroadPhaseBvh".to_string(),
],
suggestion: Some(Suggestion {
message: "replace BroadPhase with BroadPhaseBvh".to_string(),
replacement: Some("BroadPhaseBvh".to_string()),
applicability: Applicability::MachineApplicable,
}),
related: vec![],
}
}
pub fn fa903(location: Location) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA903"),
severity: Severity::Hint,
message: "Consider using step_with_events() instead of step() for frame-aware event collection".to_string(),
location,
notes: vec![
"step_with_events() returns frame-allocated contact and proximity events".to_string(),
"step() discards events and provides no frame allocation benefits".to_string(),
],
suggestion: Some(Suggestion {
message: "use step_with_events(&alloc) to get frame-allocated events".to_string(),
replacement: Some("step_with_events(&alloc)".to_string()),
applicability: Applicability::HasPlaceholders,
}),
related: vec![],
}
}
pub fn fa904(location: Location) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA904"),
severity: Severity::Warning,
message: "Ray casting may not work correctly without calling step() first to update the broad phase".to_string(),
location,
notes: vec![
"The broad phase BVH must be updated after inserting colliders".to_string(),
"Call step() once before ray casting to ensure colliders are registered".to_string(),
],
suggestion: Some(Suggestion {
message: "call physics.step() before cast_ray()".to_string(),
replacement: None,
applicability: Applicability::HasPlaceholders,
}),
related: vec![],
}
}
pub fn fa905(location: Location) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA905"),
severity: Severity::Warning,
message: "frame_alloc_slice has been replaced with frame_alloc_batch + manual copying".to_string(),
location,
notes: vec![
"frame_alloc_slice was removed in favor of more explicit batch allocation".to_string(),
"Use frame_alloc_batch() and manually copy elements for better performance".to_string(),
],
suggestion: Some(Suggestion {
message: "use frame_alloc_batch() + manual copying".to_string(),
replacement: None,
applicability: Applicability::HasPlaceholders,
}),
related: vec![],
}
}
impl Diagnostic {
pub fn builder(code: &str) -> DiagnosticBuilder {
DiagnosticBuilder::new(code)
}
}
pub struct DiagnosticBuilder {
code: DiagnosticCode,
severity: Severity,
message: Option<String>,
location: Option<Location>,
notes: Vec<String>,
suggestion: Option<Suggestion>,
related: Vec<RelatedLocation>,
}
impl DiagnosticBuilder {
pub fn new(code: &str) -> Self {
Self {
code: DiagnosticCode::new(code),
severity: Severity::Warning,
message: None,
location: None,
notes: Vec::new(),
suggestion: None,
related: Vec::new(),
}
}
pub fn severity(mut self, severity: Severity) -> Self {
self.severity = severity;
self
}
pub fn message(mut self, msg: impl Into<String>) -> Self {
self.message = Some(msg.into());
self
}
pub fn location(mut self, loc: Location) -> Self {
self.location = Some(loc);
self
}
pub fn note(mut self, note: impl Into<String>) -> Self {
self.notes.push(note.into());
self
}
pub fn suggestion(mut self, msg: impl Into<String>) -> Self {
self.suggestion = Some(Suggestion {
message: msg.into(),
replacement: None,
applicability: Applicability::Unspecified,
});
self
}
pub fn build(self) -> Diagnostic {
Diagnostic {
code: self.code,
severity: self.severity,
message: self.message.unwrap_or_default(),
location: self.location.unwrap_or(Location {
file: PathBuf::new(),
line: 0,
column: 0,
end_line: None,
end_column: None,
}),
notes: self.notes,
suggestion: self.suggestion,
related: self.related,
}
}
}
pub fn fa804(location: Location) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA804"),
severity: Severity::Error,
message: "device-local buffer mapped for CPU access".to_string(),
location,
notes: vec![
"device-local memory cannot be mapped for CPU access".to_string(),
"attempting to map device-local memory will fail at runtime".to_string(),
],
suggestion: Some(Suggestion {
message: "use MemoryType::HostVisible or MemoryType::HostCoherent for mapped buffers".to_string(),
replacement: None,
applicability: Applicability::MaybeIncorrect,
}),
related: vec![],
}
}
pub fn fa805(location: Location) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::new("FA805"),
severity: Severity::Warning,
message: "staging buffer reused across frames without reset".to_string(),
location,
notes: vec![
"reusing staging buffers across frames can lead to data corruption".to_string(),
"staging buffers should be created fresh each frame or properly reset".to_string(),
],
suggestion: Some(Suggestion {
message: "create new staging buffers each frame or properly reset them with begin_frame()".to_string(),
replacement: None,
applicability: Applicability::MaybeIncorrect,
}),
related: vec![],
}
}