selene_lib/lints/
high_cyclomatic_complexity.rs

1use super::*;
2use crate::ast_util::range;
3use std::convert::Infallible;
4
5use full_moon::{
6    ast::{self, Ast, TableConstructor},
7    visitors::Visitor,
8};
9
10use serde::Deserialize;
11
12#[derive(Clone, Copy, Deserialize)]
13pub struct HighCyclomaticComplexityConfig {
14    maximum_complexity: u16,
15}
16
17impl Default for HighCyclomaticComplexityConfig {
18    fn default() -> Self {
19        Self {
20            // eslint defaults to 20, but testing on OSS Lua shows that 20 is too aggressive
21            maximum_complexity: 40,
22        }
23    }
24}
25
26#[derive(Default)]
27pub struct HighCyclomaticComplexityLint {
28    config: HighCyclomaticComplexityConfig,
29}
30
31impl Lint for HighCyclomaticComplexityLint {
32    type Config = HighCyclomaticComplexityConfig;
33    type Error = Infallible;
34
35    const SEVERITY: Severity = Severity::Allow;
36    const LINT_TYPE: LintType = LintType::Style;
37
38    fn new(config: Self::Config) -> Result<Self, Self::Error> {
39        Ok(HighCyclomaticComplexityLint { config })
40    }
41
42    fn pass(&self, ast: &Ast, _: &Context, _: &AstContext) -> Vec<Diagnostic> {
43        let mut visitor = HighCyclomaticComplexityVisitor {
44            positions: Vec::new(),
45            config: self.config,
46        };
47
48        visitor.visit_ast(ast);
49
50        visitor
51            .positions
52            .into_iter()
53            .map(|(position, complexity)| {
54                Diagnostic::new(
55                    "high_cyclomatic_complexity",
56                    format!(
57                        "cyclomatic complexity is too high ({complexity} > {})",
58                        self.config.maximum_complexity
59                    ),
60                    Label::new(position),
61                )
62            })
63            .collect()
64    }
65}
66
67struct HighCyclomaticComplexityVisitor {
68    positions: Vec<((u32, u32), u16)>,
69    config: HighCyclomaticComplexityConfig,
70}
71
72fn count_table_complexity(table: &TableConstructor, starting_complexity: u16) -> u16 {
73    let mut complexity = starting_complexity;
74
75    for field in table.fields() {
76        #[cfg_attr(
77            feature = "force_exhaustive_checks",
78            deny(non_exhaustive_omitted_patterns)
79        )]
80        match field {
81            ast::Field::ExpressionKey { key, value, .. } => {
82                complexity = count_expression_complexity(key, complexity);
83                complexity = count_expression_complexity(value, complexity);
84            }
85
86            ast::Field::NameKey { value, .. } => {
87                complexity = count_expression_complexity(value, complexity);
88            }
89
90            ast::Field::NoKey(expression) => {
91                complexity = count_expression_complexity(expression, complexity);
92            }
93
94            _ => {}
95        }
96    }
97    complexity
98}
99
100fn count_arguments_complexity(function_args: &ast::FunctionArgs, starting_complexity: u16) -> u16 {
101    let mut complexity = starting_complexity;
102
103    #[cfg_attr(
104        feature = "force_exhaustive_checks",
105        deny(non_exhaustive_omitted_patterns)
106    )]
107    match function_args {
108        ast::FunctionArgs::Parentheses { arguments, .. } => {
109            for argument in arguments {
110                complexity = count_expression_complexity(argument, complexity)
111            }
112            complexity
113        }
114        ast::FunctionArgs::TableConstructor(table) => {
115            complexity = count_table_complexity(table, complexity);
116            complexity
117        }
118        ast::FunctionArgs::String(_) => complexity,
119        _ => complexity,
120    }
121}
122
123fn count_suffix_complexity(suffix: &ast::Suffix, starting_complexity: u16) -> u16 {
124    let mut complexity = starting_complexity;
125
126    #[cfg_attr(
127        feature = "force_exhaustive_checks",
128        deny(non_exhaustive_omitted_patterns)
129    )]
130    match suffix {
131        ast::Suffix::Index(ast::Index::Brackets { expression, .. }) => {
132            complexity = count_expression_complexity(expression, complexity)
133        }
134        ast::Suffix::Index(ast::Index::Dot { .. }) => {
135            // Dot indexing doesn't contribute to complexity
136        }
137        ast::Suffix::Call(call) => match call {
138            ast::Call::AnonymousCall(arguments) => {
139                complexity = count_arguments_complexity(arguments, complexity)
140            }
141            ast::Call::MethodCall(method_call) => {
142                complexity = count_arguments_complexity(method_call.args(), complexity)
143            }
144            _ => {}
145        },
146        _ => {}
147    }
148
149    complexity
150}
151
152fn count_expression_complexity(expression: &ast::Expression, starting_complexity: u16) -> u16 {
153    let mut complexity = starting_complexity;
154
155    #[cfg_attr(
156        feature = "force_exhaustive_checks",
157        deny(non_exhaustive_omitted_patterns)
158    )]
159    match expression {
160        ast::Expression::BinaryOperator {
161            lhs, binop, rhs, ..
162        } => {
163            #[cfg_attr(
164                feature = "force_exhaustive_checks",
165                allow(non_exhaustive_omitted_patterns)
166            )]
167            if matches!(binop, ast::BinOp::And(_) | ast::BinOp::Or(_)) {
168                complexity += 1;
169            }
170
171            complexity = count_expression_complexity(lhs, complexity);
172            complexity = count_expression_complexity(rhs, complexity);
173
174            complexity
175        }
176
177        ast::Expression::Parentheses { expression, .. } => {
178            count_expression_complexity(expression, complexity)
179        }
180
181        ast::Expression::UnaryOperator { expression, .. } => {
182            count_expression_complexity(expression, complexity)
183        }
184
185        // visit_expression already tracks this
186        ast::Expression::Function(_) => complexity,
187
188        ast::Expression::FunctionCall(call) => {
189            if let ast::Prefix::Expression(prefix_expression) = call.prefix() {
190                complexity = count_expression_complexity(prefix_expression, complexity)
191            }
192            for suffix in call.suffixes() {
193                complexity = count_suffix_complexity(suffix, complexity)
194            }
195
196            complexity
197        }
198
199        ast::Expression::Number(_) => complexity,
200        ast::Expression::String(_) => complexity,
201        ast::Expression::Symbol(_) => complexity,
202
203        ast::Expression::TableConstructor(table) => count_table_complexity(table, complexity),
204
205        ast::Expression::Var(ast::Var::Expression(var_expression)) => {
206            for suffix in var_expression.suffixes() {
207                complexity = count_suffix_complexity(suffix, complexity)
208            }
209            complexity
210        }
211
212        ast::Expression::Var(ast::Var::Name(_)) => complexity,
213
214        #[cfg(feature = "roblox")]
215        ast::Expression::IfExpression(if_expression) => {
216            complexity += 1;
217            if let Some(else_if_expressions) = if_expression.else_if_expressions() {
218                for else_if_expression in else_if_expressions {
219                    complexity += 1;
220                    complexity =
221                        count_expression_complexity(else_if_expression.expression(), complexity);
222                }
223            }
224            complexity
225        }
226
227        #[cfg(feature = "roblox")]
228        ast::Expression::InterpolatedString(interpolated_string) => {
229            for expression in interpolated_string.expressions() {
230                complexity = count_expression_complexity(expression, complexity)
231            }
232
233            complexity
234        }
235
236        #[cfg(feature = "roblox")]
237        ast::Expression::TypeAssertion { expression, .. } => {
238            count_expression_complexity(expression, complexity)
239        }
240
241        _ => complexity,
242    }
243}
244
245fn count_block_complexity(block: &ast::Block, starting_complexity: u16) -> u16 {
246    let mut complexity = starting_complexity;
247
248    // we don't immediately return from the matched blocks so that we can add in any complexity from the last statement
249    for statement in block.stmts() {
250        #[cfg_attr(
251            feature = "force_exhaustive_checks",
252            deny(non_exhaustive_omitted_patterns)
253        )]
254        match statement {
255            ast::Stmt::Assignment(assignment) => {
256                for var in assignment.variables() {
257                    if let ast::Var::Expression(var_expression) = var {
258                        for suffix in var_expression.suffixes() {
259                            complexity = count_suffix_complexity(suffix, complexity)
260                        }
261                    }
262                }
263                for expression in assignment.expressions() {
264                    complexity = count_expression_complexity(expression, complexity);
265                }
266            }
267
268            ast::Stmt::Do(do_) => {
269                complexity = count_block_complexity(do_.block(), complexity);
270            }
271
272            ast::Stmt::FunctionCall(call) => {
273                if let ast::Prefix::Expression(prefix_expression) = call.prefix() {
274                    complexity = count_expression_complexity(prefix_expression, complexity)
275                }
276                for suffix in call.suffixes() {
277                    complexity = count_suffix_complexity(suffix, complexity)
278                }
279            }
280
281            // visit_function_declaration already tracks this
282            ast::Stmt::FunctionDeclaration(_) => {}
283
284            ast::Stmt::GenericFor(generic_for) => {
285                complexity += 1;
286                for expression in generic_for.expressions() {
287                    complexity = count_expression_complexity(expression, complexity);
288                }
289                complexity = count_block_complexity(generic_for.block(), complexity);
290            }
291
292            ast::Stmt::If(if_block) => {
293                complexity += 1;
294                complexity = count_expression_complexity(if_block.condition(), complexity);
295                complexity = count_block_complexity(if_block.block(), complexity);
296
297                if let Some(else_if_statements) = if_block.else_if() {
298                    for else_if in else_if_statements {
299                        complexity += 1;
300                        complexity = count_expression_complexity(else_if.condition(), complexity);
301                        complexity = count_block_complexity(else_if.block(), complexity);
302                    }
303                }
304            }
305
306            ast::Stmt::LocalAssignment(local_assignment) => {
307                for expression in local_assignment.expressions() {
308                    complexity = count_expression_complexity(expression, complexity);
309                }
310            }
311
312            // visit_local_function tracks this
313            ast::Stmt::LocalFunction(_) => {}
314
315            ast::Stmt::NumericFor(numeric_for) => {
316                complexity += 1;
317                complexity = count_expression_complexity(numeric_for.start(), complexity);
318                complexity = count_expression_complexity(numeric_for.end(), complexity);
319
320                if let Some(step_expression) = numeric_for.step() {
321                    complexity = count_expression_complexity(step_expression, complexity);
322                }
323
324                complexity = count_block_complexity(numeric_for.block(), complexity);
325            }
326
327            ast::Stmt::Repeat(repeat_block) => {
328                complexity = count_expression_complexity(repeat_block.until(), complexity + 1);
329                complexity = count_block_complexity(repeat_block.block(), complexity);
330            }
331
332            ast::Stmt::While(while_block) => {
333                complexity = count_expression_complexity(while_block.condition(), complexity + 1);
334                complexity = count_block_complexity(while_block.block(), complexity);
335            }
336
337            #[cfg(feature = "roblox")]
338            ast::Stmt::CompoundAssignment(compound_expression) => {
339                complexity = count_expression_complexity(compound_expression.rhs(), complexity)
340            }
341
342            #[cfg(feature = "roblox")]
343            ast::Stmt::ExportedTypeDeclaration(_) => {
344                // doesn't contribute dynamic branches
345            }
346
347            #[cfg(feature = "roblox")]
348            ast::Stmt::TypeDeclaration(_) => {
349                // doesn't contain branch points
350            }
351
352            #[cfg(feature = "lua52")]
353            ast::Stmt::Goto(_) => {
354                // not a dynamic branch point itself
355            }
356
357            #[cfg(feature = "lua52")]
358            ast::Stmt::Label(_) => {
359                // not a dynamic branch point itself
360            }
361
362            _ => {}
363        }
364    }
365
366    if let Some(ast::LastStmt::Return(return_stmt)) = block.last_stmt() {
367        for return_expression in return_stmt.returns() {
368            complexity = count_expression_complexity(return_expression, complexity);
369        }
370    }
371
372    complexity
373}
374
375impl Visitor for HighCyclomaticComplexityVisitor {
376    fn visit_local_function(&mut self, local_function: &ast::LocalFunction) {
377        let complexity = count_block_complexity(local_function.body().block(), 1);
378        if complexity > self.config.maximum_complexity {
379            self.positions.push((
380                (
381                    range(local_function.function_token()).0,
382                    range(local_function.body().parameters_parentheses()).1,
383                ),
384                complexity,
385            ));
386        }
387    }
388
389    fn visit_function_declaration(&mut self, function_declaration: &ast::FunctionDeclaration) {
390        let complexity = count_block_complexity(function_declaration.body().block(), 1);
391        if complexity > self.config.maximum_complexity {
392            self.positions.push((
393                (
394                    range(function_declaration.function_token()).0,
395                    range(function_declaration.body().parameters_parentheses()).1,
396                ),
397                complexity,
398            ));
399        }
400    }
401
402    fn visit_expression(&mut self, expression: &ast::Expression) {
403        if let ast::Expression::Function(function_box) = expression {
404            let function_body = &function_box.1;
405            let complexity = count_block_complexity(function_body.block(), 1);
406            if complexity > self.config.maximum_complexity {
407                self.positions.push((
408                    (
409                        expression.start_position().unwrap().bytes() as u32,
410                        range(function_body.parameters_parentheses()).1,
411                    ),
412                    complexity,
413                ));
414            }
415        }
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use super::{super::test_util::test_lint, *};
422
423    #[test]
424    #[cfg(feature = "roblox")]
425    #[cfg_attr(debug_assertions, ignore)] // Remove these with the full_moon parser rewrite
426    fn test_high_cyclomatic_complexity() {
427        test_lint(
428            HighCyclomaticComplexityLint::new(HighCyclomaticComplexityConfig::default()).unwrap(),
429            "high_cyclomatic_complexity",
430            "high_cyclomatic_complexity",
431        );
432    }
433
434    #[test]
435    #[cfg(feature = "roblox")]
436    #[cfg_attr(debug_assertions, ignore)]
437    fn test_complex_var_expressions() {
438        test_lint(
439            HighCyclomaticComplexityLint::new(HighCyclomaticComplexityConfig::default()).unwrap(),
440            "high_cyclomatic_complexity",
441            "complex_var_expressions",
442        );
443    }
444
445    #[test]
446    fn test_lua51_basic_complexity() {
447        test_lint(
448            HighCyclomaticComplexityLint::new(HighCyclomaticComplexityConfig {
449                maximum_complexity: 1,
450            })
451            .unwrap(),
452            "high_cyclomatic_complexity",
453            "lua51_basic_complexity",
454        );
455    }
456}