Skip to main content

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        #[cfg(feature = "roblox")]
147        ast::Suffix::TypeInstantiation(_) => {}
148        _ => {}
149    }
150
151    complexity
152}
153
154fn count_expression_complexity(expression: &ast::Expression, starting_complexity: u16) -> u16 {
155    let mut complexity = starting_complexity;
156
157    #[cfg_attr(
158        feature = "force_exhaustive_checks",
159        deny(non_exhaustive_omitted_patterns)
160    )]
161    match expression {
162        ast::Expression::BinaryOperator {
163            lhs, binop, rhs, ..
164        } => {
165            #[cfg_attr(
166                feature = "force_exhaustive_checks",
167                allow(non_exhaustive_omitted_patterns)
168            )]
169            if matches!(binop, ast::BinOp::And(_) | ast::BinOp::Or(_)) {
170                complexity += 1;
171            }
172
173            complexity = count_expression_complexity(lhs, complexity);
174            complexity = count_expression_complexity(rhs, complexity);
175
176            complexity
177        }
178
179        ast::Expression::Parentheses { expression, .. } => {
180            count_expression_complexity(expression, complexity)
181        }
182
183        ast::Expression::UnaryOperator { expression, .. } => {
184            count_expression_complexity(expression, complexity)
185        }
186
187        // visit_expression already tracks this
188        ast::Expression::Function(_) => complexity,
189
190        ast::Expression::FunctionCall(call) => {
191            if let ast::Prefix::Expression(prefix_expression) = call.prefix() {
192                complexity = count_expression_complexity(prefix_expression, complexity)
193            }
194            for suffix in call.suffixes() {
195                complexity = count_suffix_complexity(suffix, complexity)
196            }
197
198            complexity
199        }
200
201        ast::Expression::Number(_) => complexity,
202        ast::Expression::String(_) => complexity,
203        ast::Expression::Symbol(_) => complexity,
204
205        ast::Expression::TableConstructor(table) => count_table_complexity(table, complexity),
206
207        ast::Expression::Var(ast::Var::Expression(var_expression)) => {
208            for suffix in var_expression.suffixes() {
209                complexity = count_suffix_complexity(suffix, complexity)
210            }
211            complexity
212        }
213
214        ast::Expression::Var(ast::Var::Name(_)) => complexity,
215
216        #[cfg(feature = "roblox")]
217        ast::Expression::IfExpression(if_expression) => {
218            complexity += 1;
219            if let Some(else_if_expressions) = if_expression.else_if_expressions() {
220                for else_if_expression in else_if_expressions {
221                    complexity += 1;
222                    complexity =
223                        count_expression_complexity(else_if_expression.expression(), complexity);
224                }
225            }
226            complexity
227        }
228
229        #[cfg(feature = "roblox")]
230        ast::Expression::InterpolatedString(interpolated_string) => {
231            for expression in interpolated_string.expressions() {
232                complexity = count_expression_complexity(expression, complexity)
233            }
234
235            complexity
236        }
237
238        #[cfg(feature = "roblox")]
239        ast::Expression::TypeAssertion { expression, .. } => {
240            count_expression_complexity(expression, complexity)
241        }
242
243        _ => complexity,
244    }
245}
246
247fn count_block_complexity(block: &ast::Block, starting_complexity: u16) -> u16 {
248    let mut complexity = starting_complexity;
249
250    // we don't immediately return from the matched blocks so that we can add in any complexity from the last statement
251    for statement in block.stmts() {
252        #[cfg_attr(
253            feature = "force_exhaustive_checks",
254            deny(non_exhaustive_omitted_patterns)
255        )]
256        match statement {
257            ast::Stmt::Assignment(assignment) => {
258                for var in assignment.variables() {
259                    if let ast::Var::Expression(var_expression) = var {
260                        for suffix in var_expression.suffixes() {
261                            complexity = count_suffix_complexity(suffix, complexity)
262                        }
263                    }
264                }
265                for expression in assignment.expressions() {
266                    complexity = count_expression_complexity(expression, complexity);
267                }
268            }
269
270            ast::Stmt::Do(do_) => {
271                complexity = count_block_complexity(do_.block(), complexity);
272            }
273
274            ast::Stmt::FunctionCall(call) => {
275                if let ast::Prefix::Expression(prefix_expression) = call.prefix() {
276                    complexity = count_expression_complexity(prefix_expression, complexity)
277                }
278                for suffix in call.suffixes() {
279                    complexity = count_suffix_complexity(suffix, complexity)
280                }
281            }
282
283            // visit_function_declaration already tracks this
284            ast::Stmt::FunctionDeclaration(_) => {}
285
286            ast::Stmt::GenericFor(generic_for) => {
287                complexity += 1;
288                for expression in generic_for.expressions() {
289                    complexity = count_expression_complexity(expression, complexity);
290                }
291                complexity = count_block_complexity(generic_for.block(), complexity);
292            }
293
294            ast::Stmt::If(if_block) => {
295                complexity += 1;
296                complexity = count_expression_complexity(if_block.condition(), complexity);
297                complexity = count_block_complexity(if_block.block(), complexity);
298
299                if let Some(else_if_statements) = if_block.else_if() {
300                    for else_if in else_if_statements {
301                        complexity += 1;
302                        complexity = count_expression_complexity(else_if.condition(), complexity);
303                        complexity = count_block_complexity(else_if.block(), complexity);
304                    }
305                }
306            }
307
308            ast::Stmt::LocalAssignment(local_assignment) => {
309                for expression in local_assignment.expressions() {
310                    complexity = count_expression_complexity(expression, complexity);
311                }
312            }
313
314            // visit_local_function tracks this
315            ast::Stmt::LocalFunction(_) => {}
316
317            ast::Stmt::NumericFor(numeric_for) => {
318                complexity += 1;
319                complexity = count_expression_complexity(numeric_for.start(), complexity);
320                complexity = count_expression_complexity(numeric_for.end(), complexity);
321
322                if let Some(step_expression) = numeric_for.step() {
323                    complexity = count_expression_complexity(step_expression, complexity);
324                }
325
326                complexity = count_block_complexity(numeric_for.block(), complexity);
327            }
328
329            ast::Stmt::Repeat(repeat_block) => {
330                complexity = count_expression_complexity(repeat_block.until(), complexity + 1);
331                complexity = count_block_complexity(repeat_block.block(), complexity);
332            }
333
334            ast::Stmt::While(while_block) => {
335                complexity = count_expression_complexity(while_block.condition(), complexity + 1);
336                complexity = count_block_complexity(while_block.block(), complexity);
337            }
338
339            #[cfg(feature = "roblox")]
340            ast::Stmt::CompoundAssignment(compound_expression) => {
341                complexity = count_expression_complexity(compound_expression.rhs(), complexity)
342            }
343
344            #[cfg(feature = "roblox")]
345            ast::Stmt::ExportedTypeDeclaration(_) => {
346                // doesn't contribute dynamic branches
347            }
348
349            #[cfg(feature = "roblox")]
350            ast::Stmt::TypeDeclaration(_) => {
351                // doesn't contain branch points
352            }
353
354            #[cfg(feature = "lua52")]
355            ast::Stmt::Goto(_) => {
356                // not a dynamic branch point itself
357            }
358
359            #[cfg(feature = "lua52")]
360            ast::Stmt::Label(_) => {
361                // not a dynamic branch point itself
362            }
363
364            #[cfg(feature = "roblox")]
365            ast::Stmt::ExportedTypeFunction(_) => {
366                // doesn't contain branch points in type declarations
367            }
368
369            #[cfg(feature = "roblox")]
370            ast::Stmt::TypeFunction(_) => {
371                // doesn't contain branch points in type declarations
372            }
373
374            _ => {}
375        }
376    }
377
378    if let Some(ast::LastStmt::Return(return_stmt)) = block.last_stmt() {
379        for return_expression in return_stmt.returns() {
380            complexity = count_expression_complexity(return_expression, complexity);
381        }
382    }
383
384    complexity
385}
386
387impl Visitor for HighCyclomaticComplexityVisitor {
388    fn visit_local_function(&mut self, local_function: &ast::LocalFunction) {
389        let complexity = count_block_complexity(local_function.body().block(), 1);
390        if complexity > self.config.maximum_complexity {
391            self.positions.push((
392                (
393                    range(local_function.function_token()).0,
394                    range(local_function.body().parameters_parentheses()).1,
395                ),
396                complexity,
397            ));
398        }
399    }
400
401    fn visit_function_declaration(&mut self, function_declaration: &ast::FunctionDeclaration) {
402        let complexity = count_block_complexity(function_declaration.body().block(), 1);
403        if complexity > self.config.maximum_complexity {
404            self.positions.push((
405                (
406                    range(function_declaration.function_token()).0,
407                    range(function_declaration.body().parameters_parentheses()).1,
408                ),
409                complexity,
410            ));
411        }
412    }
413
414    fn visit_expression(&mut self, expression: &ast::Expression) {
415        if let ast::Expression::Function(function_box) = expression {
416            let function_body = function_box.body();
417            let complexity = count_block_complexity(function_body.block(), 1);
418            if complexity > self.config.maximum_complexity {
419                self.positions.push((
420                    (
421                        expression.start_position().unwrap().bytes() as u32,
422                        range(function_body.parameters_parentheses()).1,
423                    ),
424                    complexity,
425                ));
426            }
427        }
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::{super::test_util::*, *};
434
435    #[test]
436    #[cfg(feature = "roblox")]
437    #[cfg_attr(debug_assertions, ignore)] // Remove these with the full_moon parser rewrite
438    fn test_high_cyclomatic_complexity() {
439        test_lint_config(
440            HighCyclomaticComplexityLint::new(HighCyclomaticComplexityConfig::default()).unwrap(),
441            "high_cyclomatic_complexity",
442            "high_cyclomatic_complexity",
443            TestUtilConfig::luau(),
444        );
445    }
446
447    #[test]
448    #[cfg(feature = "roblox")]
449    #[cfg_attr(debug_assertions, ignore)]
450    fn test_complex_var_expressions() {
451        test_lint_config(
452            HighCyclomaticComplexityLint::new(HighCyclomaticComplexityConfig::default()).unwrap(),
453            "high_cyclomatic_complexity",
454            "complex_var_expressions",
455            TestUtilConfig::luau(),
456        );
457    }
458
459    #[test]
460    fn test_lua51_basic_complexity() {
461        test_lint(
462            HighCyclomaticComplexityLint::new(HighCyclomaticComplexityConfig {
463                maximum_complexity: 1,
464            })
465            .unwrap(),
466            "high_cyclomatic_complexity",
467            "lua51_basic_complexity",
468        );
469    }
470}