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 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 }
348
349 #[cfg(feature = "roblox")]
350 ast::Stmt::TypeDeclaration(_) => {
351 }
353
354 #[cfg(feature = "lua52")]
355 ast::Stmt::Goto(_) => {
356 }
358
359 #[cfg(feature = "lua52")]
360 ast::Stmt::Label(_) => {
361 }
363
364 #[cfg(feature = "roblox")]
365 ast::Stmt::ExportedTypeFunction(_) => {
366 }
368
369 #[cfg(feature = "roblox")]
370 ast::Stmt::TypeFunction(_) => {
371 }
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)] 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}