use std::collections::HashMap;
use std::path::{Path, PathBuf};
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use tracing::debug;
use tree_sitter::{Node, Tree};
use crate::ast::AstExtractor;
use crate::callgraph::scanner::{ProjectScanner, ScanConfig};
use crate::error::{Result, BrrrError};
use crate::lang::LanguageRegistry;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LongMethodConfig {
pub max_lines: u32,
pub max_statements: u32,
pub max_variables: u32,
pub max_complexity: u32,
pub max_nesting: u32,
pub max_parameters: u32,
pub min_lines_for_analysis: u32,
pub context_aware: bool,
}
impl Default for LongMethodConfig {
fn default() -> Self {
Self {
max_lines: 30,
max_statements: 25,
max_variables: 10,
max_complexity: 10,
max_nesting: 4,
max_parameters: 5,
min_lines_for_analysis: 5,
context_aware: true,
}
}
}
impl LongMethodConfig {
#[must_use]
pub fn strict() -> Self {
Self {
max_lines: 20,
max_statements: 15,
max_variables: 6,
max_complexity: 6,
max_nesting: 3,
max_parameters: 4,
min_lines_for_analysis: 3,
context_aware: true,
}
}
#[must_use]
pub fn lenient() -> Self {
Self {
max_lines: 50,
max_statements: 40,
max_variables: 15,
max_complexity: 15,
max_nesting: 5,
max_parameters: 8,
min_lines_for_analysis: 10,
context_aware: true,
}
}
fn adjust_for_tests(&self) -> Self {
Self {
max_lines: self.max_lines + 30, max_statements: self.max_statements + 20,
max_variables: self.max_variables + 10, max_complexity: self.max_complexity + 5,
max_nesting: self.max_nesting + 1,
max_parameters: self.max_parameters + 3,
min_lines_for_analysis: self.min_lines_for_analysis,
context_aware: self.context_aware,
}
}
fn adjust_for_configuration(&self) -> Self {
Self {
max_lines: self.max_lines + 20,
max_statements: self.max_statements + 20,
max_variables: self.max_variables + 15, max_complexity: self.max_complexity,
max_nesting: self.max_nesting,
max_parameters: self.max_parameters,
min_lines_for_analysis: self.min_lines_for_analysis,
context_aware: self.context_aware,
}
}
fn adjust_for_factory(&self) -> Self {
Self {
max_lines: self.max_lines + 10,
max_statements: self.max_statements + 10,
max_variables: self.max_variables + 5,
max_complexity: self.max_complexity + 3,
max_nesting: self.max_nesting,
max_parameters: self.max_parameters + 3,
min_lines_for_analysis: self.min_lines_for_analysis,
context_aware: self.context_aware,
}
}
fn adjust_for_constructor(&self) -> Self {
Self {
max_lines: self.max_lines + 15,
max_statements: self.max_statements + 15,
max_variables: self.max_variables + 10, max_complexity: self.max_complexity,
max_nesting: self.max_nesting,
max_parameters: self.max_parameters + 5, min_lines_for_analysis: self.min_lines_for_analysis,
context_aware: self.context_aware,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LengthMetrics {
pub lines: u32,
pub statements: u32,
pub variables: u32,
pub parameters: u32,
pub total_span: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplexityMetrics {
pub cyclomatic: u32,
pub max_nesting: u32,
pub average_nesting: f64,
pub exit_points: u32,
pub branches: u32,
pub loops: u32,
pub boolean_operators: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MethodCategory {
Normal,
Test,
Constructor,
Configuration,
Factory,
Handler,
}
impl MethodCategory {
#[must_use]
pub fn from_name(name: &str) -> Self {
let lower = name.to_lowercase();
if lower.starts_with("test_")
|| lower.starts_with("test")
|| lower.ends_with("_test")
|| lower.ends_with("test")
|| lower.starts_with("spec_")
|| lower.ends_with("_spec")
|| lower.starts_with("it_")
|| lower.starts_with("describe_")
|| lower.starts_with("should_")
{
return Self::Test;
}
if lower == "__init__"
|| lower == "new"
|| lower == "constructor"
|| lower == "init"
|| lower.starts_with("init_")
|| lower.ends_with("_init")
{
return Self::Constructor;
}
if lower.starts_with("configure_")
|| lower.starts_with("config_")
|| lower.starts_with("setup_")
|| lower.starts_with("setup")
|| lower.ends_with("_setup")
|| lower == "setup"
|| lower.starts_with("initialize_")
{
return Self::Configuration;
}
if lower.starts_with("create_")
|| lower.starts_with("make_")
|| lower.starts_with("build_")
|| lower.starts_with("new_")
|| lower.ends_with("_factory")
|| lower.starts_with("from_")
{
return Self::Factory;
}
if lower.starts_with("on_")
|| lower.starts_with("handle_")
|| lower.ends_with("_handler")
|| lower.ends_with("_callback")
|| lower.ends_with("_listener")
{
return Self::Handler;
}
Self::Normal
}
#[must_use]
pub const fn description(&self) -> &'static str {
match self {
Self::Normal => "method",
Self::Test => "test method",
Self::Constructor => "constructor",
Self::Configuration => "configuration method",
Self::Factory => "factory method",
Self::Handler => "event handler",
}
}
}
impl std::fmt::Display for MethodCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.description())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RefactoringType {
ExtractMethod,
ReplaceTempWithQuery,
DecomposeConditional,
ReplaceLoopWithPipeline,
ExtractGuardClause,
ExtractSetup,
ExtractValidation,
SplitByResponsibility,
IntroduceParameterObject,
Simplify,
}
impl std::fmt::Display for RefactoringType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ExtractMethod => write!(f, "Extract Method"),
Self::ReplaceTempWithQuery => write!(f, "Replace Temp with Query"),
Self::DecomposeConditional => write!(f, "Decompose Conditional"),
Self::ReplaceLoopWithPipeline => write!(f, "Replace Loop with Pipeline"),
Self::ExtractGuardClause => write!(f, "Extract Guard Clause"),
Self::ExtractSetup => write!(f, "Extract Setup"),
Self::ExtractValidation => write!(f, "Extract Validation"),
Self::SplitByResponsibility => write!(f, "Split by Responsibility"),
Self::IntroduceParameterObject => write!(f, "Introduce Parameter Object"),
Self::Simplify => write!(f, "Simplify"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RefactoringSuggestion {
pub refactoring_type: RefactoringType,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub line_range: Option<(usize, usize)>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suggested_name: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub required_parameters: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub return_variable: Option<String>,
pub confidence: f64,
pub priority: u32,
}
impl RefactoringSuggestion {
fn extract_method(
start_line: usize,
end_line: usize,
suggested_name: Option<String>,
description: String,
confidence: f64,
) -> Self {
Self {
refactoring_type: RefactoringType::ExtractMethod,
description,
line_range: Some((start_line, end_line)),
suggested_name,
required_parameters: Vec::new(),
return_variable: None,
confidence,
priority: 5,
}
}
fn extract_guard(line: usize, condition: &str) -> Self {
Self {
refactoring_type: RefactoringType::ExtractGuardClause,
description: format!("Extract early return for condition: {}", condition),
line_range: Some((line, line)),
suggested_name: None,
required_parameters: Vec::new(),
return_variable: None,
confidence: 0.8,
priority: 7,
}
}
fn decompose_conditional(start_line: usize, end_line: usize, branches: u32) -> Self {
Self {
refactoring_type: RefactoringType::DecomposeConditional,
description: format!(
"Decompose complex conditional with {} branches into separate methods",
branches
),
line_range: Some((start_line, end_line)),
suggested_name: None,
required_parameters: Vec::new(),
return_variable: None,
confidence: 0.75,
priority: 6,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BlockType {
StatementBlock,
LoopBody,
ConditionalBranch,
TryBlock,
ExceptionHandler,
ValidationBlock,
SetupBlock,
CleanupBlock,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractionCandidate {
pub block_type: BlockType,
pub start_line: usize,
pub end_line: usize,
pub statement_count: u32,
pub defined_variables: Vec<String>,
pub used_variables: Vec<String>,
pub live_out_variables: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suggested_name: Option<String>,
pub viability_score: f64,
pub reason: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LongMethodSeverity {
Minor,
Moderate,
Major,
Critical,
}
impl LongMethodSeverity {
fn from_violations(
lines_exceeded: bool,
complexity_exceeded: bool,
nesting_exceeded: bool,
variables_exceeded: bool,
) -> Self {
let count = [lines_exceeded, complexity_exceeded, nesting_exceeded, variables_exceeded]
.iter()
.filter(|&&x| x)
.count();
match count {
0 => Self::Minor,
1 => Self::Moderate,
2 => Self::Major,
_ => Self::Critical,
}
}
#[must_use]
pub const fn color_code(&self) -> &'static str {
match self {
Self::Minor => "\x1b[33m", Self::Moderate => "\x1b[91m", Self::Major => "\x1b[31m", Self::Critical => "\x1b[35m", }
}
#[must_use]
pub const fn description(&self) -> &'static str {
match self {
Self::Minor => "Consider refactoring when convenient",
Self::Moderate => "Should be refactored",
Self::Major => "Prioritize refactoring",
Self::Critical => "Refactor immediately",
}
}
}
impl std::fmt::Display for LongMethodSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Minor => write!(f, "minor"),
Self::Moderate => write!(f, "moderate"),
Self::Major => write!(f, "major"),
Self::Critical => write!(f, "critical"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Violation {
TooLong { lines: u32, threshold: u32 },
TooManyStatements { count: u32, threshold: u32 },
TooManyVariables { count: u32, threshold: u32 },
TooManyParameters { count: u32, threshold: u32 },
TooComplex { complexity: u32, threshold: u32 },
TooDeep { depth: u32, threshold: u32 },
}
impl std::fmt::Display for Violation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TooLong { lines, threshold } => {
write!(f, "Too long: {} lines (threshold: {})", lines, threshold)
}
Self::TooManyStatements { count, threshold } => {
write!(f, "Too many statements: {} (threshold: {})", count, threshold)
}
Self::TooManyVariables { count, threshold } => {
write!(f, "Too many variables: {} (threshold: {})", count, threshold)
}
Self::TooManyParameters { count, threshold } => {
write!(f, "Too many parameters: {} (threshold: {})", count, threshold)
}
Self::TooComplex { complexity, threshold } => {
write!(f, "Too complex: {} (threshold: {})", complexity, threshold)
}
Self::TooDeep { depth, threshold } => {
write!(f, "Too deep nesting: {} (threshold: {})", depth, threshold)
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LongMethodFinding {
pub function_name: String,
pub file: PathBuf,
pub line: usize,
pub end_line: usize,
pub length: LengthMetrics,
pub complexity: ComplexityMetrics,
pub category: MethodCategory,
pub severity: LongMethodSeverity,
pub violations: Vec<Violation>,
pub suggestions: Vec<RefactoringSuggestion>,
pub extraction_candidates: Vec<ExtractionCandidate>,
pub score: f64,
}
impl LongMethodFinding {
fn calculate_score(length: &LengthMetrics, complexity: &ComplexityMetrics) -> f64 {
let line_score = f64::from(length.lines) * 1.0;
let statement_score = f64::from(length.statements) * 0.8;
let variable_score = f64::from(length.variables) * 1.5;
let complexity_score = f64::from(complexity.cyclomatic) * 2.0;
let nesting_score = f64::from(complexity.max_nesting) * 3.0;
line_score + statement_score + variable_score + complexity_score + nesting_score
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LongMethodStats {
pub total_methods: usize,
pub long_methods: usize,
pub percentage_long: f64,
pub average_lines: f64,
pub max_lines: u32,
pub average_complexity: f64,
pub max_complexity: u32,
pub severity_distribution: HashMap<String, usize>,
pub violation_counts: HashMap<String, usize>,
}
impl LongMethodStats {
fn from_findings(
total_methods: usize,
findings: &[LongMethodFinding],
all_lines: &[u32],
all_complexity: &[u32],
) -> Self {
let long_methods = findings.len();
let percentage_long = if total_methods > 0 {
(long_methods as f64 / total_methods as f64) * 100.0
} else {
0.0
};
let (average_lines, max_lines) = if all_lines.is_empty() {
(0.0, 0)
} else {
let sum: u64 = all_lines.iter().map(|&l| u64::from(l)).sum();
(
sum as f64 / all_lines.len() as f64,
*all_lines.iter().max().unwrap_or(&0),
)
};
let (average_complexity, max_complexity) = if all_complexity.is_empty() {
(0.0, 0)
} else {
let sum: u64 = all_complexity.iter().map(|&c| u64::from(c)).sum();
(
sum as f64 / all_complexity.len() as f64,
*all_complexity.iter().max().unwrap_or(&0),
)
};
let mut severity_distribution = HashMap::new();
let mut violation_counts = HashMap::new();
for finding in findings {
*severity_distribution
.entry(finding.severity.to_string())
.or_insert(0) += 1;
for violation in &finding.violations {
let key = match violation {
Violation::TooLong { .. } => "too_long",
Violation::TooManyStatements { .. } => "too_many_statements",
Violation::TooManyVariables { .. } => "too_many_variables",
Violation::TooManyParameters { .. } => "too_many_parameters",
Violation::TooComplex { .. } => "too_complex",
Violation::TooDeep { .. } => "too_deep",
};
*violation_counts.entry(key.to_string()).or_insert(0) += 1;
}
}
Self {
total_methods,
long_methods,
percentage_long,
average_lines,
max_lines,
average_complexity,
max_complexity,
severity_distribution,
violation_counts,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LongMethodAnalysis {
pub path: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
pub findings: Vec<LongMethodFinding>,
pub stats: LongMethodStats,
pub config: LongMethodConfig,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<LongMethodError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LongMethodError {
pub file: PathBuf,
pub message: String,
}
const STATEMENT_NODES: &[&str] = &[
"expression_statement", "return_statement", "if_statement", "for_statement",
"while_statement", "try_statement", "with_statement", "assert_statement",
"raise_statement", "pass_statement", "break_statement", "continue_statement",
"import_statement", "import_from_statement", "global_statement",
"nonlocal_statement", "delete_statement", "match_statement",
"return_statement", "switch_statement", "for_in_statement", "do_statement",
"throw_statement", "variable_declaration", "lexical_declaration",
"let_declaration", "return_expression", "if_expression", "match_expression",
"for_expression", "while_expression", "loop_expression", "break_expression",
"continue_expression", "macro_invocation",
"go_statement", "select_statement", "defer_statement", "var_declaration",
"short_var_declaration", "assignment_statement",
"switch_expression", "enhanced_for_statement", "local_variable_declaration",
"goto_statement", "declaration", "compound_statement",
];
const DECISION_NODES: &[&str] = &[
"if_statement", "if_expression", "elif_clause", "else_if_clause",
"for_statement", "for_expression", "for_in_statement", "enhanced_for_statement",
"while_statement", "while_expression",
"switch_statement", "switch_expression", "match_expression", "match_statement",
"case_clause", "match_arm",
"try_statement", "except_clause", "catch_clause",
"conditional_expression", "ternary_expression",
"do_statement", "loop_expression",
];
const NESTING_NODES: &[&str] = &[
"if_statement", "if_expression", "elif_clause",
"for_statement", "for_expression", "for_in_statement",
"while_statement", "while_expression",
"try_statement", "except_clause", "catch_clause",
"with_statement",
"switch_statement", "switch_expression", "match_expression",
"do_statement", "loop_expression",
"lambda", "arrow_function", "closure_expression",
"list_comprehension", "dictionary_comprehension", "generator_expression",
];
const EXIT_NODES: &[&str] = &[
"return_statement", "return_expression",
"throw_statement", "raise_statement",
"break_statement", "break_expression",
];
const LOOP_NODES: &[&str] = &[
"for_statement", "for_expression", "for_in_statement", "enhanced_for_statement",
"while_statement", "while_expression",
"do_statement", "loop_expression",
];
const VARIABLE_NODES: &[&str] = &[
"assignment", "augmented_assignment",
"variable_declaration", "lexical_declaration", "variable_declarator",
"let_declaration",
"var_declaration", "short_var_declaration", "var_spec",
"local_variable_declaration",
"declaration", "init_declarator",
];
const FUNCTION_NODES: &[&str] = &[
"function_definition", "function_declaration", "method_definition",
"arrow_function", "function_item", "method_declaration",
"constructor_declaration",
];
struct MethodAnalyzer<'a> {
source: &'a [u8],
lines: Vec<&'a str>,
language: &'a str,
}
impl<'a> MethodAnalyzer<'a> {
fn new(source: &'a [u8], language: &'a str) -> Self {
let source_str = std::str::from_utf8(source).unwrap_or("");
let lines: Vec<&str> = source_str.lines().collect();
Self { source, lines, language }
}
fn count_sloc(&self, start_line: usize, end_line: usize) -> u32 {
let mut count = 0u32;
for i in start_line..=end_line.min(self.lines.len().saturating_sub(1)) {
let line = self.lines.get(i).map(|s| s.trim()).unwrap_or("");
if !line.is_empty() && !is_comment_line(line) {
count += 1;
}
}
count
}
fn analyze_function(
&self,
node: Node,
start_line: usize,
end_line: usize,
) -> (LengthMetrics, ComplexityMetrics, Vec<ExtractionCandidate>) {
let mut statements = 0u32;
let mut variables = 0u32;
let mut decisions = 0u32;
let mut max_nesting = 0u32;
let mut exit_points = 0u32;
let mut branches = 0u32;
let mut loops = 0u32;
let mut nesting_sum = 0u64;
let mut nesting_count = 0usize;
let mut extraction_candidates = Vec::new();
self.walk_function(
node,
0,
&mut statements,
&mut variables,
&mut decisions,
&mut max_nesting,
&mut exit_points,
&mut branches,
&mut loops,
&mut nesting_sum,
&mut nesting_count,
&mut extraction_candidates,
);
let parameters = self.count_parameters(node);
let sloc = self.count_sloc(start_line.saturating_sub(1), end_line.saturating_sub(1));
let length = LengthMetrics {
lines: sloc,
statements,
variables,
parameters,
total_span: (end_line - start_line + 1) as u32,
};
let average_nesting = if nesting_count > 0 {
nesting_sum as f64 / nesting_count as f64
} else {
0.0
};
let boolean_operators = self.count_boolean_operators(node);
let complexity = ComplexityMetrics {
cyclomatic: decisions + 1, max_nesting,
average_nesting,
exit_points,
branches,
loops,
boolean_operators,
};
(length, complexity, extraction_candidates)
}
#[allow(clippy::too_many_arguments)]
fn walk_function(
&self,
node: Node,
depth: u32,
statements: &mut u32,
variables: &mut u32,
decisions: &mut u32,
max_nesting: &mut u32,
exit_points: &mut u32,
branches: &mut u32,
loops: &mut u32,
nesting_sum: &mut u64,
nesting_count: &mut usize,
candidates: &mut Vec<ExtractionCandidate>,
) {
if depth > 50 {
return;
}
let kind = node.kind();
let line = node.start_position().row + 1;
if STATEMENT_NODES.contains(&kind) {
*statements += 1;
}
if VARIABLE_NODES.contains(&kind) && !FUNCTION_NODES.contains(&kind) {
*variables += count_declarators(node);
}
if DECISION_NODES.contains(&kind) {
*decisions += 1;
}
let new_depth = if NESTING_NODES.contains(&kind) {
depth + 1
} else {
depth
};
if new_depth > *max_nesting {
*max_nesting = new_depth;
}
*nesting_sum += u64::from(new_depth);
*nesting_count += 1;
if EXIT_NODES.contains(&kind) {
*exit_points += 1;
}
if matches!(kind, "if_statement" | "if_expression" | "elif_clause" | "else_clause") {
*branches += 1;
}
if LOOP_NODES.contains(&kind) {
*loops += 1;
if let Some(body) = self.find_body_node(node) {
let body_start = body.start_position().row + 1;
let body_end = body.end_position().row + 1;
let body_statements = self.count_body_statements(body);
if body_statements >= 3 {
candidates.push(ExtractionCandidate {
block_type: BlockType::LoopBody,
start_line: body_start,
end_line: body_end,
statement_count: body_statements,
defined_variables: Vec::new(),
used_variables: Vec::new(),
live_out_variables: Vec::new(),
suggested_name: self.suggest_loop_method_name(node),
viability_score: if body_statements >= 5 { 0.8 } else { 0.6 },
reason: format!("Loop body with {} statements", body_statements),
});
}
}
}
if matches!(kind, "if_statement" | "if_expression") {
let branch_count = self.count_conditional_branches(node);
if branch_count >= 3 {
let end_line = node.end_position().row + 1;
candidates.push(ExtractionCandidate {
block_type: BlockType::ConditionalBranch,
start_line: line,
end_line,
statement_count: self.count_body_statements(node),
defined_variables: Vec::new(),
used_variables: Vec::new(),
live_out_variables: Vec::new(),
suggested_name: None,
viability_score: 0.7,
reason: format!("Complex conditional with {} branches", branch_count),
});
}
}
if kind == "try_statement" {
let end_line = node.end_position().row + 1;
let body_statements = self.count_body_statements(node);
if body_statements >= 3 {
candidates.push(ExtractionCandidate {
block_type: BlockType::TryBlock,
start_line: line,
end_line,
statement_count: body_statements,
defined_variables: Vec::new(),
used_variables: Vec::new(),
live_out_variables: Vec::new(),
suggested_name: None,
viability_score: 0.65,
reason: format!("Try block with {} statements", body_statements),
});
}
}
if FUNCTION_NODES.contains(&kind) && depth > 0 {
return;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
self.walk_function(
child,
new_depth,
statements,
variables,
decisions,
max_nesting,
exit_points,
branches,
loops,
nesting_sum,
nesting_count,
candidates,
);
}
}
fn count_parameters(&self, node: Node) -> u32 {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if matches!(
child.kind(),
"parameters" | "formal_parameters" | "parameter_list" | "function_parameters"
) {
return count_param_children(child, self.source);
}
}
0
}
fn count_boolean_operators(&self, node: Node) -> u32 {
let mut count = 0u32;
self.walk_for_boolean_ops(node, &mut count, 0);
count
}
fn walk_for_boolean_ops(&self, node: Node, count: &mut u32, depth: u32) {
if depth > 50 {
return;
}
let kind = node.kind();
if matches!(
kind,
"boolean_operator" | "binary_expression" | "logical_expression"
) {
if let Ok(text) = node.utf8_text(self.source) {
if text.contains("&&")
|| text.contains("||")
|| text.contains(" and ")
|| text.contains(" or ")
{
*count += 1;
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
self.walk_for_boolean_ops(child, count, depth + 1);
}
}
fn find_body_node<'b>(&self, node: Node<'b>) -> Option<Node<'b>> {
if let Some(body) = node.child_by_field_name("body") {
return Some(body);
}
if let Some(consequence) = node.child_by_field_name("consequence") {
return Some(consequence);
}
node.child_by_field_name("block")
}
fn count_body_statements(&self, node: Node) -> u32 {
let mut count = 0u32;
self.walk_count_statements(node, &mut count, 0);
count
}
fn walk_count_statements(&self, node: Node, count: &mut u32, depth: u32) {
if depth > 30 {
return;
}
if STATEMENT_NODES.contains(&node.kind()) {
*count += 1;
}
if FUNCTION_NODES.contains(&node.kind()) && depth > 0 {
return;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
self.walk_count_statements(child, count, depth + 1);
}
}
fn count_conditional_branches(&self, node: Node) -> u32 {
let mut count = 1u32; let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if matches!(child.kind(), "elif_clause" | "else_if_clause" | "else_clause") {
count += 1;
}
}
count
}
fn suggest_loop_method_name(&self, node: Node) -> Option<String> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "identifier" || child.kind() == "pattern" {
if let Ok(text) = child.utf8_text(self.source) {
if !text.is_empty() && text != "i" && text != "j" && text != "_" {
return Some(format!("process_{}", text));
}
}
}
}
None
}
}
fn is_comment_line(line: &str) -> bool {
let trimmed = line.trim();
trimmed.starts_with('#')
|| trimmed.starts_with("//")
|| trimmed.starts_with("/*")
|| trimmed.starts_with('*')
|| trimmed.starts_with("*/")
|| trimmed.starts_with("'''")
|| trimmed.starts_with("\"\"\"")
}
fn count_declarators(node: Node) -> u32 {
let mut count = 0u32;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if matches!(
child.kind(),
"variable_declarator" | "init_declarator" | "var_spec" | "identifier"
) {
count += 1;
}
}
count.max(1)
}
fn count_param_children(params_node: Node, source: &[u8]) -> u32 {
let mut count = 0u32;
let mut cursor = params_node.walk();
for child in params_node.children(&mut cursor) {
match child.kind() {
"(" | ")" | "," | "comment" => {}
"identifier" => {
let text = child.utf8_text(source).unwrap_or("");
if text != "self" && text != "cls" && text != "this" {
count += 1;
}
}
_ if child.kind().contains("parameter") => {
let text = child.utf8_text(source).unwrap_or("");
if !text.starts_with("self") && !text.starts_with("cls") && !text.starts_with("this") {
count += 1;
}
}
_ => {
if child.child_count() > 0 {
count += 1;
}
}
}
}
count
}
fn generate_suggestions(
length: &LengthMetrics,
complexity: &ComplexityMetrics,
candidates: &[ExtractionCandidate],
config: &LongMethodConfig,
) -> Vec<RefactoringSuggestion> {
let mut suggestions = Vec::new();
if length.parameters > config.max_parameters {
suggestions.push(RefactoringSuggestion {
refactoring_type: RefactoringType::IntroduceParameterObject,
description: format!(
"Introduce a parameter object to group {} parameters",
length.parameters
),
line_range: None,
suggested_name: Some("Options".to_string()),
required_parameters: Vec::new(),
return_variable: None,
confidence: 0.8,
priority: 7,
});
}
if length.variables > config.max_variables {
suggestions.push(RefactoringSuggestion {
refactoring_type: RefactoringType::ExtractSetup,
description: format!(
"Extract initialization of {} variables into a setup method",
length.variables
),
line_range: None,
suggested_name: Some("setup_context".to_string()),
required_parameters: Vec::new(),
return_variable: None,
confidence: 0.7,
priority: 5,
});
}
if complexity.max_nesting > config.max_nesting {
suggestions.push(RefactoringSuggestion {
refactoring_type: RefactoringType::ExtractGuardClause,
description: format!(
"Extract guard clauses to reduce nesting from {} levels",
complexity.max_nesting
),
line_range: None,
suggested_name: None,
required_parameters: Vec::new(),
return_variable: None,
confidence: 0.85,
priority: 8,
});
}
if complexity.cyclomatic > config.max_complexity {
suggestions.push(RefactoringSuggestion {
refactoring_type: RefactoringType::SplitByResponsibility,
description: format!(
"Split method with complexity {} into smaller focused methods",
complexity.cyclomatic
),
line_range: None,
suggested_name: None,
required_parameters: Vec::new(),
return_variable: None,
confidence: 0.75,
priority: 6,
});
}
if complexity.branches > 3 {
suggestions.push(RefactoringSuggestion {
refactoring_type: RefactoringType::DecomposeConditional,
description: format!(
"Decompose {} conditional branches into separate methods",
complexity.branches
),
line_range: None,
suggested_name: None,
required_parameters: Vec::new(),
return_variable: None,
confidence: 0.7,
priority: 5,
});
}
for candidate in candidates {
if candidate.viability_score >= 0.6 {
let description = match candidate.block_type {
BlockType::LoopBody => format!(
"Extract loop body (lines {}-{}) into {}",
candidate.start_line,
candidate.end_line,
candidate.suggested_name.as_deref().unwrap_or("a helper method")
),
BlockType::ConditionalBranch => format!(
"Extract conditional logic (lines {}-{}) into a dedicated method",
candidate.start_line, candidate.end_line
),
BlockType::TryBlock => format!(
"Extract try/except block (lines {}-{}) into an error-handling method",
candidate.start_line, candidate.end_line
),
_ => format!(
"Extract lines {}-{} into a separate method",
candidate.start_line, candidate.end_line
),
};
suggestions.push(RefactoringSuggestion {
refactoring_type: RefactoringType::ExtractMethod,
description,
line_range: Some((candidate.start_line, candidate.end_line)),
suggested_name: candidate.suggested_name.clone(),
required_parameters: candidate.used_variables.clone(),
return_variable: candidate.live_out_variables.first().cloned(),
confidence: candidate.viability_score,
priority: 4,
});
}
}
suggestions.sort_by(|a, b| b.priority.cmp(&a.priority));
suggestions
}
pub fn detect_long_methods(
path: impl AsRef<Path>,
language: Option<&str>,
config: Option<LongMethodConfig>,
) -> Result<LongMethodAnalysis> {
let path = path.as_ref();
let config = config.unwrap_or_default();
if !path.exists() {
return Err(BrrrError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Path not found: {}", path.display()),
)));
}
if path.is_file() {
return detect_long_methods_in_file(path, &config);
}
let path_str = path
.to_str()
.ok_or_else(|| BrrrError::InvalidArgument("Invalid path encoding".to_string()))?;
let scanner = ProjectScanner::new(path_str)?;
let scan_config = if let Some(lang) = language {
ScanConfig::for_language(lang)
} else {
ScanConfig::default()
};
let scan_result = scanner.scan_with_config(&scan_config)?;
if scan_result.files.is_empty() {
return Err(BrrrError::InvalidArgument(format!(
"No source files found in {} (filter: {:?})",
path.display(),
language
)));
}
debug!(
"Analyzing {} files for long methods",
scan_result.files.len()
);
let results: Vec<(Vec<LongMethodFinding>, Vec<LongMethodError>, Vec<u32>, Vec<u32>)> =
scan_result
.files
.par_iter()
.map(|file| analyze_file_methods(file, &config))
.collect();
let mut all_findings = Vec::new();
let mut all_errors = Vec::new();
let mut all_lines = Vec::new();
let mut all_complexity = Vec::new();
let mut total_methods = 0usize;
for (findings, errors, lines, complexity) in results {
total_methods += lines.len();
all_findings.extend(findings);
all_errors.extend(errors);
all_lines.extend(lines);
all_complexity.extend(complexity);
}
let stats = LongMethodStats::from_findings(total_methods, &all_findings, &all_lines, &all_complexity);
all_findings.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
Ok(LongMethodAnalysis {
path: path.to_path_buf(),
language: language.map(String::from),
findings: all_findings,
stats,
config,
errors: all_errors,
})
}
pub fn detect_long_methods_in_file(
path: impl AsRef<Path>,
config: &LongMethodConfig,
) -> Result<LongMethodAnalysis> {
let path = path.as_ref();
if !path.exists() {
return Err(BrrrError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("File not found: {}", path.display()),
)));
}
let (findings, errors, all_lines, all_complexity) = analyze_file_methods(path, config);
let total_methods = all_lines.len();
let stats = LongMethodStats::from_findings(total_methods, &findings, &all_lines, &all_complexity);
let registry = LanguageRegistry::global();
let language = registry
.detect_language(path)
.map(|l| l.name().to_string());
Ok(LongMethodAnalysis {
path: path.to_path_buf(),
language,
findings,
stats,
config: config.clone(),
errors,
})
}
fn analyze_file_methods(
file: &Path,
config: &LongMethodConfig,
) -> (Vec<LongMethodFinding>, Vec<LongMethodError>, Vec<u32>, Vec<u32>) {
let mut findings = Vec::new();
let mut errors = Vec::new();
let mut all_lines = Vec::new();
let mut all_complexity = Vec::new();
let source = match std::fs::read(file) {
Ok(s) => s,
Err(e) => {
errors.push(LongMethodError {
file: file.to_path_buf(),
message: format!("Failed to read file: {}", e),
});
return (findings, errors, all_lines, all_complexity);
}
};
let module = match AstExtractor::extract_file(file) {
Ok(m) => m,
Err(e) => {
errors.push(LongMethodError {
file: file.to_path_buf(),
message: format!("Failed to parse: {}", e),
});
return (findings, errors, all_lines, all_complexity);
}
};
let registry = LanguageRegistry::global();
let lang = match registry.detect_language(file) {
Some(l) => l,
None => {
errors.push(LongMethodError {
file: file.to_path_buf(),
message: "Unknown language".to_string(),
});
return (findings, errors, all_lines, all_complexity);
}
};
let mut parser = match lang.parser() {
Ok(p) => p,
Err(e) => {
errors.push(LongMethodError {
file: file.to_path_buf(),
message: format!("Parser error: {}", e),
});
return (findings, errors, all_lines, all_complexity);
}
};
let tree = match parser.parse(&source, None) {
Some(t) => t,
None => {
errors.push(LongMethodError {
file: file.to_path_buf(),
message: "Parse failed".to_string(),
});
return (findings, errors, all_lines, all_complexity);
}
};
let analyzer = MethodAnalyzer::new(&source, lang.name());
for func in &module.functions {
if let Some(finding) = analyze_single_method(
&analyzer,
&tree,
file,
&func.name,
func.line_number,
func.end_line_number.unwrap_or(func.line_number),
config,
) {
all_lines.push(finding.length.lines);
all_complexity.push(finding.complexity.cyclomatic);
if !finding.violations.is_empty() {
findings.push(finding);
}
}
}
for class in &module.classes {
for method in &class.methods {
let qualified_name = format!("{}.{}", class.name, method.name);
if let Some(mut finding) = analyze_single_method(
&analyzer,
&tree,
file,
&qualified_name,
method.line_number,
method.end_line_number.unwrap_or(method.line_number),
config,
) {
finding.function_name = qualified_name;
all_lines.push(finding.length.lines);
all_complexity.push(finding.complexity.cyclomatic);
if !finding.violations.is_empty() {
findings.push(finding);
}
}
}
}
(findings, errors, all_lines, all_complexity)
}
fn analyze_single_method(
analyzer: &MethodAnalyzer,
tree: &Tree,
file: &Path,
name: &str,
start_line: usize,
end_line: usize,
base_config: &LongMethodConfig,
) -> Option<LongMethodFinding> {
if start_line == 0 {
return None;
}
let func_node = find_function_node(tree.root_node(), start_line)?;
let (length, complexity, candidates) = analyzer.analyze_function(func_node, start_line, end_line);
if length.lines < base_config.min_lines_for_analysis {
return None;
}
let base_name = name.split('.').last().unwrap_or(name);
let category = MethodCategory::from_name(base_name);
let config = if base_config.context_aware {
match category {
MethodCategory::Test => base_config.adjust_for_tests(),
MethodCategory::Configuration => base_config.adjust_for_configuration(),
MethodCategory::Factory => base_config.adjust_for_factory(),
MethodCategory::Constructor => base_config.adjust_for_constructor(),
_ => base_config.clone(),
}
} else {
base_config.clone()
};
let mut violations = Vec::new();
if length.lines > config.max_lines {
violations.push(Violation::TooLong {
lines: length.lines,
threshold: config.max_lines,
});
}
if length.statements > config.max_statements {
violations.push(Violation::TooManyStatements {
count: length.statements,
threshold: config.max_statements,
});
}
if length.variables > config.max_variables {
violations.push(Violation::TooManyVariables {
count: length.variables,
threshold: config.max_variables,
});
}
if length.parameters > config.max_parameters {
violations.push(Violation::TooManyParameters {
count: length.parameters,
threshold: config.max_parameters,
});
}
if complexity.cyclomatic > config.max_complexity {
violations.push(Violation::TooComplex {
complexity: complexity.cyclomatic,
threshold: config.max_complexity,
});
}
if complexity.max_nesting > config.max_nesting {
violations.push(Violation::TooDeep {
depth: complexity.max_nesting,
threshold: config.max_nesting,
});
}
let severity = LongMethodSeverity::from_violations(
length.lines > config.max_lines,
complexity.cyclomatic > config.max_complexity,
complexity.max_nesting > config.max_nesting,
length.variables > config.max_variables,
);
let suggestions = generate_suggestions(&length, &complexity, &candidates, &config);
let score = LongMethodFinding::calculate_score(&length, &complexity);
Some(LongMethodFinding {
function_name: name.to_string(),
file: file.to_path_buf(),
line: start_line,
end_line,
length,
complexity,
category,
severity,
violations,
suggestions,
extraction_candidates: candidates,
score,
})
}
fn find_function_node(node: Node, target_line: usize) -> Option<Node> {
let node_start = node.start_position().row + 1;
let node_end = node.end_position().row + 1;
if FUNCTION_NODES.contains(&node.kind()) && node_start == target_line {
return Some(node);
}
if target_line < node_start || target_line > node_end {
return None;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if let Some(found) = find_function_node(child, target_line) {
return Some(found);
}
}
None
}
pub fn format_long_method_summary(analysis: &LongMethodAnalysis) -> String {
let mut output = String::new();
output.push_str("=== Long Method Analysis ===\n\n");
output.push_str(&format!("Path: {}\n", analysis.path.display()));
if let Some(ref lang) = analysis.language {
output.push_str(&format!("Language: {}\n", lang));
}
output.push('\n');
output.push_str("Statistics:\n");
output.push_str(&format!(
" Total methods: {}\n",
analysis.stats.total_methods
));
output.push_str(&format!(
" Long methods: {} ({:.1}%)\n",
analysis.stats.long_methods, analysis.stats.percentage_long
));
output.push_str(&format!(
" Average lines: {:.1}\n",
analysis.stats.average_lines
));
output.push_str(&format!(" Max lines: {}\n", analysis.stats.max_lines));
output.push_str(&format!(
" Average complexity: {:.1}\n",
analysis.stats.average_complexity
));
output.push_str(&format!(
" Max complexity: {}\n",
analysis.stats.max_complexity
));
output.push('\n');
if !analysis.stats.severity_distribution.is_empty() {
output.push_str("Severity Distribution:\n");
for (severity, count) in &analysis.stats.severity_distribution {
output.push_str(&format!(" {}: {}\n", severity, count));
}
output.push('\n');
}
if analysis.findings.is_empty() {
output.push_str("No long methods found.\n");
} else {
output.push_str(&format!("Findings ({}):\n\n", analysis.findings.len()));
for finding in &analysis.findings {
output.push_str(&format!(
"{}{}{} at {}:{}-{}\n",
finding.severity.color_code(),
finding.function_name,
"\x1b[0m",
finding.file.display(),
finding.line,
finding.end_line
));
output.push_str(&format!(
" Category: {}\n",
finding.category.description()
));
output.push_str(&format!(
" Severity: {} - {}\n",
finding.severity,
finding.severity.description()
));
output.push_str(&format!(
" Metrics: {} lines, {} statements, {} vars, complexity {}, nesting {}\n",
finding.length.lines,
finding.length.statements,
finding.length.variables,
finding.complexity.cyclomatic,
finding.complexity.max_nesting
));
if !finding.violations.is_empty() {
output.push_str(" Violations:\n");
for v in &finding.violations {
output.push_str(&format!(" - {}\n", v));
}
}
let top_suggestions: Vec<_> = finding.suggestions.iter().take(3).collect();
if !top_suggestions.is_empty() {
output.push_str(" Suggestions:\n");
for s in top_suggestions {
output.push_str(&format!(" - [{}] {}\n", s.refactoring_type, s.description));
}
}
output.push('\n');
}
}
output
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
fn create_temp_file(content: &str, extension: &str) -> NamedTempFile {
let mut file = tempfile::Builder::new()
.suffix(extension)
.tempfile()
.expect("Failed to create temp file");
file.write_all(content.as_bytes())
.expect("Failed to write to temp file");
file
}
#[test]
fn test_method_category_detection() {
assert_eq!(MethodCategory::from_name("test_something"), MethodCategory::Test);
assert_eq!(MethodCategory::from_name("TestCase"), MethodCategory::Test);
assert_eq!(MethodCategory::from_name("__init__"), MethodCategory::Constructor);
assert_eq!(MethodCategory::from_name("new"), MethodCategory::Constructor);
assert_eq!(MethodCategory::from_name("setup_database"), MethodCategory::Configuration);
assert_eq!(MethodCategory::from_name("create_user"), MethodCategory::Factory);
assert_eq!(MethodCategory::from_name("on_click"), MethodCategory::Handler);
assert_eq!(MethodCategory::from_name("process_data"), MethodCategory::Normal);
}
#[test]
fn test_simple_method_not_flagged() {
let source = r#"
def simple():
return 42
"#;
let file = create_temp_file(source, ".py");
let result = detect_long_methods_in_file(file.path(), &LongMethodConfig::default());
assert!(result.is_ok());
let analysis = result.unwrap();
assert!(analysis.findings.is_empty());
}
#[test]
fn test_long_method_detected() {
let mut lines = vec!["def long_function():".to_string()];
for i in 0..40 {
lines.push(format!(" x{} = {}", i, i));
}
lines.push(" return x0".to_string());
let source = lines.join("\n");
let file = create_temp_file(&source, ".py");
let result = detect_long_methods_in_file(file.path(), &LongMethodConfig::default());
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.findings.len(), 1);
assert_eq!(analysis.findings[0].function_name, "long_function");
assert!(analysis.findings[0]
.violations
.iter()
.any(|v| matches!(v, Violation::TooLong { .. })));
}
#[test]
fn test_complex_method_detected() {
let source = r#"
def complex_func(a, b, c, d, e, f, g):
if a > 0:
if b > 0:
if c > 0:
if d > 0:
if e > 0:
return "deep"
return "shallow"
"#;
let file = create_temp_file(source, ".py");
let result = detect_long_methods_in_file(file.path(), &LongMethodConfig::default());
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.findings.len(), 1);
assert!(analysis.findings[0]
.violations
.iter()
.any(|v| matches!(v, Violation::TooDeep { .. })));
}
#[test]
fn test_test_method_adjusted_thresholds() {
let mut lines = vec!["def test_something():".to_string()];
for i in 0..45 {
lines.push(format!(" x{} = {}", i, i));
}
lines.push(" assert x0 == 0".to_string());
let source = lines.join("\n");
let file = create_temp_file(&source, ".py");
let config = LongMethodConfig {
context_aware: true,
..LongMethodConfig::default()
};
let result = detect_long_methods_in_file(file.path(), &config);
assert!(result.is_ok());
let analysis = result.unwrap();
let too_long_violations: Vec<_> = analysis
.findings
.iter()
.flat_map(|f| &f.violations)
.filter(|v| matches!(v, Violation::TooLong { .. }))
.collect();
assert!(too_long_violations.is_empty());
}
#[test]
fn test_severity_calculation() {
assert_eq!(
LongMethodSeverity::from_violations(false, false, false, false),
LongMethodSeverity::Minor
);
assert_eq!(
LongMethodSeverity::from_violations(true, false, false, false),
LongMethodSeverity::Moderate
);
assert_eq!(
LongMethodSeverity::from_violations(true, true, false, false),
LongMethodSeverity::Major
);
assert_eq!(
LongMethodSeverity::from_violations(true, true, true, false),
LongMethodSeverity::Critical
);
}
#[test]
fn test_config_presets() {
let default = LongMethodConfig::default();
let strict = LongMethodConfig::strict();
let lenient = LongMethodConfig::lenient();
assert!(strict.max_lines < default.max_lines);
assert!(lenient.max_lines > default.max_lines);
}
#[test]
fn test_extraction_candidates() {
let source = r#"
def process_items(items):
results = []
for item in items:
validated = validate(item)
transformed = transform(validated)
saved = save(transformed)
results.append(saved)
return results
"#;
let file = create_temp_file(source, ".py");
let result = detect_long_methods_in_file(file.path(), &LongMethodConfig::default());
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.stats.total_methods, 1);
}
#[test]
fn test_statistics_calculation() {
let mut lines = vec!["def func1():".to_string()];
for i in 0..35 {
lines.push(format!(" x{} = {}", i, i));
}
lines.push(" return x0".to_string());
lines.push("".to_string());
lines.push("def func2():".to_string());
lines.push(" a = 1".to_string());
lines.push(" b = 2".to_string());
lines.push(" c = 3".to_string());
lines.push(" d = 4".to_string());
lines.push(" return a + b + c + d".to_string());
let source = lines.join("\n");
let file = create_temp_file(&source, ".py");
let result = detect_long_methods_in_file(file.path(), &LongMethodConfig::default());
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.stats.total_methods, 2);
assert_eq!(analysis.stats.long_methods, 1);
}
#[test]
fn test_format_summary() {
let analysis = LongMethodAnalysis {
path: PathBuf::from("test.py"),
language: Some("python".to_string()),
findings: vec![],
stats: LongMethodStats {
total_methods: 10,
long_methods: 2,
percentage_long: 20.0,
average_lines: 15.0,
max_lines: 45,
average_complexity: 5.0,
max_complexity: 12,
severity_distribution: HashMap::new(),
violation_counts: HashMap::new(),
},
config: LongMethodConfig::default(),
errors: vec![],
};
let summary = format_long_method_summary(&analysis);
assert!(summary.contains("Long Method Analysis"));
assert!(summary.contains("Total methods: 10"));
assert!(summary.contains("Long methods: 2"));
}
#[test]
fn test_nonexistent_file() {
let result = detect_long_methods("/nonexistent/path/file.py", None, None);
assert!(result.is_err());
}
#[test]
fn test_empty_file() {
let source = "# Just a comment\n";
let file = create_temp_file(source, ".py");
let result = detect_long_methods_in_file(file.path(), &LongMethodConfig::default());
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.stats.total_methods, 0);
assert!(analysis.findings.is_empty());
}
#[test]
fn test_typescript_method() {
let source = r#"
function processData(input: string): number {
const lines = input.split('\n');
let total = 0;
for (const line of lines) {
const value = parseInt(line);
if (!isNaN(value)) {
total += value;
}
}
return total;
}
"#;
let file = create_temp_file(source, ".ts");
let result = detect_long_methods_in_file(file.path(), &LongMethodConfig::default());
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.stats.total_methods, 1);
}
}