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 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 }
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 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 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 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 ast::Stmt::LocalFunction(_) => {}
316
317 #[cfg(feature = "roblox")]
318 ast::Stmt::ConstAssignment(const_assignment) => {
319 for expression in const_assignment.expressions() {
320 complexity = count_expression_complexity(expression, complexity);
321 }
322 }
323 #[cfg(feature = "roblox")]
324 ast::Stmt::ConstFunction(_) => {
325 }
327
328 ast::Stmt::NumericFor(numeric_for) => {
329 complexity += 1;
330 complexity = count_expression_complexity(numeric_for.start(), complexity);
331 complexity = count_expression_complexity(numeric_for.end(), complexity);
332
333 if let Some(step_expression) = numeric_for.step() {
334 complexity = count_expression_complexity(step_expression, complexity);
335 }
336
337 complexity = count_block_complexity(numeric_for.block(), complexity);
338 }
339
340 ast::Stmt::Repeat(repeat_block) => {
341 complexity = count_expression_complexity(repeat_block.until(), complexity + 1);
342 complexity = count_block_complexity(repeat_block.block(), complexity);
343 }
344
345 ast::Stmt::While(while_block) => {
346 complexity = count_expression_complexity(while_block.condition(), complexity + 1);
347 complexity = count_block_complexity(while_block.block(), complexity);
348 }
349
350 #[cfg(feature = "roblox")]
351 ast::Stmt::CompoundAssignment(compound_expression) => {
352 complexity = count_expression_complexity(compound_expression.rhs(), complexity)
353 }
354
355 #[cfg(feature = "roblox")]
356 ast::Stmt::ExportedTypeDeclaration(_) => {
357 }
359
360 #[cfg(feature = "roblox")]
361 ast::Stmt::TypeDeclaration(_) => {
362 }
364
365 #[cfg(feature = "lua52")]
366 ast::Stmt::Goto(_) => {
367 }
369
370 #[cfg(feature = "lua52")]
371 ast::Stmt::Label(_) => {
372 }
374
375 #[cfg(feature = "roblox")]
376 ast::Stmt::ExportedTypeFunction(_) => {
377 }
379
380 #[cfg(feature = "roblox")]
381 ast::Stmt::TypeFunction(_) => {
382 }
384
385 _ => {}
386 }
387 }
388
389 if let Some(ast::LastStmt::Return(return_stmt)) = block.last_stmt() {
390 for return_expression in return_stmt.returns() {
391 complexity = count_expression_complexity(return_expression, complexity);
392 }
393 }
394
395 complexity
396}
397
398impl Visitor for HighCyclomaticComplexityVisitor {
399 fn visit_local_function(&mut self, local_function: &ast::LocalFunction) {
400 let complexity = count_block_complexity(local_function.body().block(), 1);
401 if complexity > self.config.maximum_complexity {
402 self.positions.push((
403 (
404 range(local_function.function_token()).0,
405 range(local_function.body().parameters_parentheses()).1,
406 ),
407 complexity,
408 ));
409 }
410 }
411
412 fn visit_function_declaration(&mut self, function_declaration: &ast::FunctionDeclaration) {
413 let complexity = count_block_complexity(function_declaration.body().block(), 1);
414 if complexity > self.config.maximum_complexity {
415 self.positions.push((
416 (
417 range(function_declaration.function_token()).0,
418 range(function_declaration.body().parameters_parentheses()).1,
419 ),
420 complexity,
421 ));
422 }
423 }
424
425 fn visit_expression(&mut self, expression: &ast::Expression) {
426 if let ast::Expression::Function(function_box) = expression {
427 let function_body = function_box.body();
428 let complexity = count_block_complexity(function_body.block(), 1);
429 if complexity > self.config.maximum_complexity {
430 self.positions.push((
431 (
432 expression.start_position().unwrap().bytes() as u32,
433 range(function_body.parameters_parentheses()).1,
434 ),
435 complexity,
436 ));
437 }
438 }
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use super::{super::test_util::*, *};
445
446 #[test]
447 #[cfg(feature = "roblox")]
448 #[cfg_attr(debug_assertions, ignore)] fn test_high_cyclomatic_complexity() {
450 test_lint_config(
451 HighCyclomaticComplexityLint::new(HighCyclomaticComplexityConfig::default()).unwrap(),
452 "high_cyclomatic_complexity",
453 "high_cyclomatic_complexity",
454 TestUtilConfig::luau(),
455 );
456 }
457
458 #[test]
459 #[cfg(feature = "roblox")]
460 #[cfg_attr(debug_assertions, ignore)]
461 fn test_complex_var_expressions() {
462 test_lint_config(
463 HighCyclomaticComplexityLint::new(HighCyclomaticComplexityConfig::default()).unwrap(),
464 "high_cyclomatic_complexity",
465 "complex_var_expressions",
466 TestUtilConfig::luau(),
467 );
468 }
469
470 #[test]
471 fn test_lua51_basic_complexity() {
472 test_lint(
473 HighCyclomaticComplexityLint::new(HighCyclomaticComplexityConfig {
474 maximum_complexity: 1,
475 })
476 .unwrap(),
477 "high_cyclomatic_complexity",
478 "lua51_basic_complexity",
479 );
480 }
481}