use crate::core::{DebtType, Language};
use once_cell::sync::Lazy;
use regex::Regex;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct SuppressionContext {
pub active_blocks: Vec<SuppressionBlock>,
pub line_suppressions: HashMap<usize, SuppressionRule>,
pub unclosed_blocks: Vec<UnclosedBlock>,
pub function_allows: HashMap<usize, FunctionAllow>,
}
#[derive(Debug, Clone)]
pub struct SuppressionBlock {
pub start_line: usize,
pub end_line: Option<usize>,
pub debt_types: Vec<DebtType>,
pub reason: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SuppressionRule {
pub debt_types: Vec<DebtType>,
pub reason: Option<String>,
pub applies_to_next_line: bool,
}
#[derive(Debug, Clone)]
pub struct UnclosedBlock {
pub file: PathBuf,
pub start_line: usize,
}
#[derive(Debug, Clone)]
pub struct FunctionAllow {
pub debt_types: Vec<DebtType>,
pub reason: String,
pub annotation_line: usize,
}
#[derive(Debug, Clone)]
pub struct SuppressionStats {
pub total_suppressions: usize,
pub suppressions_by_type: HashMap<DebtType, usize>,
pub unclosed_blocks: Vec<UnclosedBlock>,
}
impl SuppressionContext {
pub fn new() -> Self {
Self {
active_blocks: Vec::new(),
line_suppressions: HashMap::new(),
unclosed_blocks: Vec::new(),
function_allows: HashMap::new(),
}
}
pub fn is_function_allowed(&self, function_start_line: usize, debt_type: &DebtType) -> bool {
for offset in 1..=5 {
if function_start_line < offset {
break;
}
let check_line = function_start_line - offset;
if let Some(allow) = self.function_allows.get(&check_line) {
return debt_type_matches(debt_type, &allow.debt_types);
}
}
false
}
pub fn get_function_allow_reason(
&self,
function_start_line: usize,
debt_type: &DebtType,
) -> Option<&str> {
for offset in 1..=5 {
if function_start_line < offset {
break;
}
let check_line = function_start_line - offset;
if let Some(allow) = self.function_allows.get(&check_line) {
if debt_type_matches(debt_type, &allow.debt_types) {
return Some(&allow.reason);
}
}
}
None
}
pub fn is_suppressed(&self, line: usize, debt_type: &DebtType) -> bool {
[
self.is_in_suppression_block(line, debt_type),
self.has_line_suppression(line, debt_type),
self.has_next_line_suppression(line, debt_type),
]
.into_iter()
.any(|suppressed| suppressed)
}
fn is_in_suppression_block(&self, line: usize, debt_type: &DebtType) -> bool {
self.active_blocks
.iter()
.filter(|block| line_within_block(line, block))
.any(|block| debt_type_matches(debt_type, &block.debt_types))
}
fn has_line_suppression(&self, line: usize, debt_type: &DebtType) -> bool {
self.line_suppressions
.get(&line)
.is_some_and(|rule| debt_type_matches(debt_type, &rule.debt_types))
}
fn has_next_line_suppression(&self, line: usize, debt_type: &DebtType) -> bool {
(line > 0)
.then(|| self.line_suppressions.get(&(line - 1)))
.flatten()
.is_some_and(|rule| {
rule.applies_to_next_line && debt_type_matches(debt_type, &rule.debt_types)
})
}
pub fn get_stats(&self) -> SuppressionStats {
let completed_blocks = self.count_completed_blocks();
let all_debt_types = self.collect_all_debt_types();
let total_suppressions = completed_blocks + self.line_suppressions.len();
let suppressions_by_type = self.count_suppressions_by_type(all_debt_types);
SuppressionStats {
total_suppressions,
suppressions_by_type,
unclosed_blocks: self.unclosed_blocks.clone(),
}
}
fn count_completed_blocks(&self) -> usize {
self.active_blocks
.iter()
.filter(|block| block.end_line.is_some())
.count()
}
fn collect_all_debt_types(&self) -> Vec<DebtType> {
let block_types = self
.active_blocks
.iter()
.filter(|block| block.end_line.is_some())
.flat_map(|block| block.debt_types.iter().cloned());
let line_types = self
.line_suppressions
.values()
.flat_map(|rule| rule.debt_types.iter().cloned());
block_types.chain(line_types).collect()
}
fn count_suppressions_by_type(&self, debt_types: Vec<DebtType>) -> HashMap<DebtType, usize> {
debt_types
.into_iter()
.fold(HashMap::new(), |mut acc, debt_type| {
*acc.entry(debt_type).or_insert(0) += 1;
acc
})
}
}
impl Default for SuppressionContext {
fn default() -> Self {
Self::new()
}
}
fn line_within_block(line: usize, block: &SuppressionBlock) -> bool {
if line == 0 && block.start_line == 1 {
return true;
}
line >= block.start_line && block.end_line.is_some_and(|end| line <= end)
}
fn debt_type_matches(debt_type: &DebtType, allowed_types: &[DebtType]) -> bool {
if allowed_types.is_empty() {
return true;
}
allowed_types
.iter()
.any(|allowed| std::mem::discriminant(allowed) == std::mem::discriminant(debt_type))
}
struct SuppressionPatterns {
block_start: Regex,
block_end: Regex,
line: Regex,
next_line: Regex,
function_allow: Regex,
}
impl SuppressionPatterns {
fn new(language: Language) -> Self {
let comment_prefix = get_comment_prefix_pattern(language);
Self {
block_start: Regex::new(&format!(
r"(?m)^\s*{comment_prefix}\s*debtmap:ignore-start(?:\s*\[([\w,*]+)\])?(?:\s*--\s*(.*))?$"
)).unwrap(),
block_end: Regex::new(&format!(
r"(?m)^\s*{comment_prefix}\s*debtmap:ignore-end\s*$"
)).unwrap(),
line: Regex::new(&format!(
r"(?m){comment_prefix}\s*debtmap:ignore(?:\s*\[([\w,*]+)\])?(?:\s*--\s*(.*))?$"
)).unwrap(),
next_line: Regex::new(&format!(
r"(?m)^\s*{comment_prefix}\s*debtmap:ignore-next-line(?:\s*\[([\w,*]+)\])?(?:\s*--\s*(.*))?$"
)).unwrap(),
function_allow: Regex::new(&format!(
r"(?m)^\s*{comment_prefix}\s*debtmap:ignore\s*\[([\w,*]+)\]\s*-{{1,2}}\s*(.+)$"
)).unwrap(),
}
}
}
fn get_comment_prefix_pattern(language: Language) -> &'static str {
match language {
Language::Python => "#",
Language::Rust => r"///?",
_ => "//",
}
}
enum LineParseResult {
BlockStart(usize, Vec<DebtType>, Option<String>),
BlockEnd(usize),
NextLineSuppression(usize, Vec<DebtType>, Option<String>),
LineSuppression(usize, Vec<DebtType>, Option<String>),
FunctionAllowAnnotation(usize, Vec<DebtType>, String),
None,
}
fn parse_line(line: &str, line_number: usize, patterns: &SuppressionPatterns) -> LineParseResult {
try_parse_block_start(line, line_number, patterns)
.or_else(|| try_parse_block_end(line, line_number, patterns))
.or_else(|| try_parse_next_line(line, line_number, patterns))
.or_else(|| try_parse_function_allow(line, line_number, patterns))
.or_else(|| try_parse_line_suppression(line, line_number, patterns))
.unwrap_or(LineParseResult::None)
}
fn try_parse_block_start(
line: &str,
line_number: usize,
patterns: &SuppressionPatterns,
) -> Option<LineParseResult> {
patterns.block_start.captures(line).map(|captures| {
LineParseResult::BlockStart(
line_number,
parse_debt_types(captures.get(1).map(|m| m.as_str())),
captures.get(2).map(|m| m.as_str().to_string()),
)
})
}
fn try_parse_block_end(
line: &str,
line_number: usize,
patterns: &SuppressionPatterns,
) -> Option<LineParseResult> {
patterns
.block_end
.is_match(line)
.then_some(LineParseResult::BlockEnd(line_number))
}
fn try_parse_next_line(
line: &str,
line_number: usize,
patterns: &SuppressionPatterns,
) -> Option<LineParseResult> {
patterns.next_line.captures(line).map(|captures| {
LineParseResult::NextLineSuppression(
line_number,
parse_debt_types(captures.get(1).map(|m| m.as_str())),
captures.get(2).map(|m| m.as_str().to_string()),
)
})
}
fn try_parse_line_suppression(
line: &str,
line_number: usize,
patterns: &SuppressionPatterns,
) -> Option<LineParseResult> {
patterns.line.captures(line).map(|captures| {
LineParseResult::LineSuppression(
line_number,
parse_debt_types(captures.get(1).map(|m| m.as_str())),
captures.get(2).map(|m| m.as_str().to_string()),
)
})
}
fn try_parse_function_allow(
line: &str,
line_number: usize,
patterns: &SuppressionPatterns,
) -> Option<LineParseResult> {
patterns.function_allow.captures(line).map(|captures| {
LineParseResult::FunctionAllowAnnotation(
line_number,
parse_debt_types(captures.get(1).map(|m| m.as_str())),
captures
.get(2)
.map(|m| m.as_str().trim().to_string())
.unwrap_or_default(),
)
})
}
fn process_parsed_line(
result: LineParseResult,
context: &mut SuppressionContext,
open_blocks: &mut Vec<(usize, Vec<DebtType>, Option<String>)>,
) {
use LineParseResult::*;
match result {
BlockStart(ln, types, reason) => open_blocks.push((ln, types, reason)),
BlockEnd(end_line) => handle_block_end(context, open_blocks, end_line),
NextLineSuppression(ln, types, reason) => {
add_line_suppression(context, ln, types, reason, true)
}
LineSuppression(ln, types, reason) => {
add_line_suppression(context, ln, types, reason, false)
}
FunctionAllowAnnotation(ln, types, reason) => {
context.function_allows.insert(
ln,
FunctionAllow {
debt_types: types,
reason,
annotation_line: ln,
},
);
}
None => {}
}
}
fn handle_block_end(
context: &mut SuppressionContext,
open_blocks: &mut Vec<(usize, Vec<DebtType>, Option<String>)>,
end_line: usize,
) {
if let Some((start_line, debt_types, reason)) = open_blocks.pop() {
context.active_blocks.push(SuppressionBlock {
start_line,
end_line: Some(end_line),
debt_types,
reason,
});
}
}
fn add_line_suppression(
context: &mut SuppressionContext,
line: usize,
debt_types: Vec<DebtType>,
reason: Option<String>,
applies_to_next_line: bool,
) {
context.line_suppressions.insert(
line,
SuppressionRule {
debt_types,
reason,
applies_to_next_line,
},
);
}
fn create_unclosed_blocks(
open_blocks: Vec<(usize, Vec<DebtType>, Option<String>)>,
file: &Path,
) -> Vec<UnclosedBlock> {
open_blocks
.into_iter()
.map(|(start_line, _, _)| UnclosedBlock {
file: file.to_path_buf(),
start_line,
})
.collect()
}
pub fn parse_suppression_comments(
content: &str,
language: Language,
file: &Path,
) -> SuppressionContext {
let patterns = SuppressionPatterns::new(language);
let mut context = SuppressionContext::new();
let mut open_blocks: Vec<(usize, Vec<DebtType>, Option<String>)> = Vec::new();
for (line_number, line) in content
.lines()
.enumerate()
.map(|(idx, line)| (idx + 1, line))
{
let result = parse_line(line, line_number, &patterns);
process_parsed_line(result, &mut context, &mut open_blocks);
}
context.unclosed_blocks = create_unclosed_blocks(open_blocks, file);
context
}
static DEBT_TYPE_MAP: Lazy<HashMap<&'static str, Vec<DebtType>>> = Lazy::new(|| {
let mut map = HashMap::new();
map.insert("todo", vec![DebtType::Todo { reason: None }]);
map.insert("fixme", vec![DebtType::Fixme { reason: None }]);
map.insert("smell", vec![DebtType::CodeSmell { smell_type: None }]);
map.insert("codesmell", vec![DebtType::CodeSmell { smell_type: None }]);
map.insert(
"duplication",
vec![DebtType::Duplication {
instances: 0,
total_lines: 0,
}],
);
map.insert(
"duplicate",
vec![DebtType::Duplication {
instances: 0,
total_lines: 0,
}],
);
map.insert(
"complexity",
vec![
DebtType::Complexity {
cyclomatic: 0,
cognitive: 0,
},
DebtType::ComplexityHotspot {
cyclomatic: 0,
cognitive: 0,
},
],
);
map.insert(
"hotspot",
vec![DebtType::ComplexityHotspot {
cyclomatic: 0,
cognitive: 0,
}],
);
map.insert(
"dependency",
vec![DebtType::Dependency {
dependency_type: None,
}],
);
map.insert(
"testing",
vec![DebtType::TestingGap {
coverage: 0.0,
cyclomatic: 0,
cognitive: 0,
}],
);
map.insert(
"coverage",
vec![DebtType::TestingGap {
coverage: 0.0,
cyclomatic: 0,
cognitive: 0,
}],
);
map.insert(
"untested",
vec![DebtType::TestingGap {
coverage: 0.0,
cyclomatic: 0,
cognitive: 0,
}],
);
map.insert(
"god_object",
vec![DebtType::GodObject {
methods: 0,
fields: None,
responsibilities: 0,
god_object_score: 0.0,
lines: 0,
}],
);
map.insert(
"godobject",
vec![DebtType::GodObject {
methods: 0,
fields: None,
responsibilities: 0,
god_object_score: 0.0,
lines: 0,
}],
);
map
});
fn parse_debt_types(types_str: Option<&str>) -> Vec<DebtType> {
let Some(types) = types_str else {
return vec![]; };
if types == "*" {
return vec![]; }
types
.split(',')
.flat_map(|t| parse_single_debt_type(t.trim()))
.collect()
}
fn parse_single_debt_type(type_str: &str) -> Vec<DebtType> {
DEBT_TYPE_MAP
.get(type_str.to_lowercase().as_str())
.cloned()
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_parse_block_suppression() {
let content = r#"
// debtmap:ignore-start
// TODO: This should be suppressed
// FIXME: This too
// debtmap:ignore-end
// TODO: This should not be suppressed
"#;
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
assert_eq!(context.active_blocks.len(), 1);
assert_eq!(context.active_blocks[0].start_line, 2);
assert_eq!(context.active_blocks[0].end_line, Some(5));
assert!(context.is_suppressed(3, &DebtType::Todo { reason: None }));
assert!(context.is_suppressed(4, &DebtType::Fixme { reason: None }));
assert!(!context.is_suppressed(6, &DebtType::Todo { reason: None }));
}
#[test]
fn test_parse_line_suppression() {
let content = r#"
// TODO: Not suppressed
// TODO: Suppressed // debtmap:ignore
// FIXME: Also not suppressed
"#;
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
assert!(!context.is_suppressed(2, &DebtType::Todo { reason: None }));
assert!(context.is_suppressed(3, &DebtType::Todo { reason: None }));
assert!(!context.is_suppressed(4, &DebtType::Fixme { reason: None }));
}
#[test]
fn test_parse_next_line_suppression() {
let content = r#"
// debtmap:ignore-next-line
// TODO: This should be suppressed
// TODO: This should not be suppressed
"#;
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
assert!(context.is_suppressed(3, &DebtType::Todo { reason: None }));
assert!(!context.is_suppressed(4, &DebtType::Todo { reason: None }));
}
#[test]
fn test_type_specific_suppression() {
let content = r#"
// debtmap:ignore-start[todo]
// TODO: Suppressed
// FIXME: Not suppressed
// debtmap:ignore-end
"#;
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
assert!(context.is_suppressed(3, &DebtType::Todo { reason: None }));
assert!(!context.is_suppressed(4, &DebtType::Fixme { reason: None }));
}
#[test]
fn test_suppression_with_reason() {
let content = r#"
// debtmap:ignore-start -- Test fixture
// TODO: Suppressed with reason
// debtmap:ignore-end
"#;
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
assert_eq!(
context.active_blocks[0].reason,
Some("Test fixture".to_string())
);
}
#[test]
fn test_unclosed_block_detection() {
let content = format!(
"{}{}{}",
"// debtmap:", "ignore-start\n", "// TODO: In unclosed block\n"
);
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(&content, Language::Rust, &file);
assert_eq!(context.unclosed_blocks.len(), 1);
assert_eq!(context.unclosed_blocks[0].start_line, 1);
}
#[test]
fn test_python_comment_syntax() {
let content = r#"
# debtmap:ignore-start
# TODO: Python TODO
# debtmap:ignore-end
"#;
let file = PathBuf::from("test.py");
let context = parse_suppression_comments(content, Language::Python, &file);
assert_eq!(context.active_blocks.len(), 1);
assert!(context.is_suppressed(3, &DebtType::Todo { reason: None }));
}
#[test]
fn test_wildcard_suppression() {
let content = "// TODO: Test // debtmap:ignore[*]";
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
assert!(context.is_suppressed(1, &DebtType::Todo { reason: None }));
assert!(context.is_suppressed(1, &DebtType::Fixme { reason: None }));
assert!(context.is_suppressed(1, &DebtType::CodeSmell { smell_type: None }));
}
#[test]
fn test_create_unclosed_blocks() {
let open_blocks = vec![
(
10,
vec![DebtType::Todo { reason: None }],
Some("reason1".to_string()),
),
(25, vec![DebtType::Fixme { reason: None }], None),
(
42,
vec![
DebtType::CodeSmell { smell_type: None },
DebtType::Complexity {
cyclomatic: 10,
cognitive: 8,
},
],
Some("reason2".to_string()),
),
];
let file = Path::new("test_file.rs");
let unclosed = create_unclosed_blocks(open_blocks, file);
assert_eq!(unclosed.len(), 3);
assert_eq!(unclosed[0].start_line, 10);
assert_eq!(unclosed[0].file, PathBuf::from("test_file.rs"));
assert_eq!(unclosed[1].start_line, 25);
assert_eq!(unclosed[2].start_line, 42);
}
#[test]
fn test_create_unclosed_blocks_empty() {
let open_blocks = vec![];
let file = Path::new("empty.rs");
let unclosed = create_unclosed_blocks(open_blocks, file);
assert!(unclosed.is_empty());
}
#[test]
fn test_testing_debt_type_suppression() {
let content = r#"
// debtmap:ignore-start[testing]
fn untested_function() {}
// debtmap:ignore-end
"#;
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
let testing_gap = DebtType::TestingGap {
coverage: 0.25,
cyclomatic: 10,
cognitive: 15,
};
assert!(context.is_suppressed(3, &testing_gap));
}
#[test]
fn test_coverage_alias_suppression() {
let content = "fn foo() {} // debtmap:ignore[coverage]";
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
let testing_gap = DebtType::TestingGap {
coverage: 0.0,
cyclomatic: 5,
cognitive: 8,
};
assert!(context.is_suppressed(1, &testing_gap));
}
#[test]
fn test_function_allow_basic() {
let content = r#"
// debtmap:ignore[testing] -- Orchestration function; callees are tested
async fn run_loop() {}
"#;
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
assert_eq!(context.function_allows.len(), 1);
let allow = context.function_allows.get(&2).unwrap();
assert_eq!(allow.reason, "Orchestration function; callees are tested");
assert!(!allow.debt_types.is_empty());
}
#[test]
fn test_function_allow_is_function_allowed() {
let content = r#"
// debtmap:ignore[testing] -- Pure logic extracted and tested
async fn orchestration_function() {}
"#;
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
let testing_gap = DebtType::TestingGap {
coverage: 0.25,
cyclomatic: 11,
cognitive: 19,
};
assert!(context.is_function_allowed(3, &testing_gap));
assert!(!context.is_function_allowed(10, &testing_gap));
}
#[test]
fn test_function_allow_get_reason() {
let content = r#"
// debtmap:ignore[complexity,testing] -- State machine with exhaustive matching
fn complex_match() {}
"#;
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
let testing_gap = DebtType::TestingGap {
coverage: 0.5,
cyclomatic: 15,
cognitive: 20,
};
let reason = context.get_function_allow_reason(3, &testing_gap);
assert_eq!(reason, Some("State machine with exhaustive matching"));
}
#[test]
fn test_function_allow_requires_reason() {
let content = r#"
// debtmap:ignore[testing]
fn no_reason() {}
"#;
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
assert!(context.function_allows.is_empty());
}
#[test]
fn test_function_allow_multiple_types() {
let content = r#"
// debtmap:ignore[testing,complexity] -- Async orchestration with inherent complexity
async fn run_loop() {}
"#;
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
let testing_gap = DebtType::TestingGap {
coverage: 0.25,
cyclomatic: 11,
cognitive: 19,
};
let complexity = DebtType::Complexity {
cyclomatic: 11,
cognitive: 19,
};
assert!(context.is_function_allowed(3, &testing_gap));
assert!(context.is_function_allowed(3, &complexity));
}
#[test]
fn test_function_allow_wildcard() {
let content = r#"
// debtmap:ignore[*] -- Legacy code pending refactor
fn legacy_function() {}
"#;
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
let testing_gap = DebtType::TestingGap {
coverage: 0.0,
cyclomatic: 20,
cognitive: 30,
};
let complexity = DebtType::Complexity {
cyclomatic: 20,
cognitive: 30,
};
let todo = DebtType::Todo { reason: None };
assert!(context.is_function_allowed(3, &testing_gap));
assert!(context.is_function_allowed(3, &complexity));
assert!(context.is_function_allowed(3, &todo));
}
#[test]
fn test_function_allow_doc_comment() {
let content = r#"
/// debtmap:ignore[complexity,coverage] -- I/O shell function
async fn invoke() {}
"#;
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
let testing_gap = DebtType::TestingGap {
coverage: 0.0,
cyclomatic: 6,
cognitive: 28,
};
let complexity = DebtType::Complexity {
cyclomatic: 6,
cognitive: 28,
};
assert!(context.is_function_allowed(3, &testing_gap));
assert!(context.is_function_allowed(3, &complexity));
let reason = context.get_function_allow_reason(3, &testing_gap);
assert_eq!(reason, Some("I/O shell function"));
}
#[test]
fn test_suppression_block_doc_comment() {
let content = r#"
/// debtmap:ignore-start[testing]
fn untested() {}
/// debtmap:ignore-end
"#;
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
let testing_gap = DebtType::TestingGap {
coverage: 0.0,
cyclomatic: 5,
cognitive: 10,
};
assert!(context.is_suppressed(3, &testing_gap));
}
#[test]
fn test_function_ignore_single_dash() {
let content = r#"
// debtmap:ignore[testing] - I/O dispatcher with no extractable pure logic
async fn run_command() {}
"#;
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
let testing_gap = DebtType::TestingGap {
coverage: 0.0,
cyclomatic: 6,
cognitive: 25,
};
assert!(context.is_function_allowed(3, &testing_gap));
let reason = context.get_function_allow_reason(3, &testing_gap);
assert_eq!(
reason,
Some("I/O dispatcher with no extractable pure logic")
);
}
#[test]
fn test_god_object_suppression() {
let content = r#"
// debtmap:ignore-start[god_object]
// Large file with many related functions
fn function1() {}
fn function2() {}
// debtmap:ignore-end
"#;
let file = PathBuf::from("classifier.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
let god_object = DebtType::GodObject {
methods: 122,
fields: None,
responsibilities: 13,
god_object_score: 37.79,
lines: 3057,
};
assert!(context.is_suppressed(4, &god_object));
assert!(context.is_suppressed(5, &god_object));
}
#[test]
fn test_god_object_function_allow() {
let content = r#"
// debtmap:ignore[god_object] - Related pure classification functions for method analysis
// Module documentation...
"#;
let file = PathBuf::from("classifier.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
let god_object = DebtType::GodObject {
methods: 122,
fields: None,
responsibilities: 13,
god_object_score: 37.79,
lines: 3057,
};
assert!(context.is_function_allowed(3, &god_object));
let reason = context.get_function_allow_reason(3, &god_object);
assert_eq!(
reason,
Some("Related pure classification functions for method analysis")
);
}
#[test]
fn test_godobject_alias() {
let content = "fn big_module() {} // debtmap:ignore[godobject]";
let file = PathBuf::from("test.rs");
let context = parse_suppression_comments(content, Language::Rust, &file);
let god_object = DebtType::GodObject {
methods: 50,
fields: Some(10),
responsibilities: 5,
god_object_score: 25.0,
lines: 1000,
};
assert!(context.is_suppressed(1, &god_object));
}
}