From d242c471d0200ac621288cffca5c9ece10f6fd11 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Thu, 18 Jun 2026 14:11:24 +0300 Subject: [PATCH 1/9] refactor: migrate cyclomatic_complexity (#250) --- lib/main.dart | 6 + .../cyclomatic_complexity_rule.dart | 76 ++++--- .../cyclomatic_complexity_parameters.dart | 7 + .../cyclomatic_complexity_flow_visitor.dart | 104 ++++++--- .../cyclomatic_complexity_visitor.dart | 44 ++++ lint_test/cyclomatic_complexity_test.dart | 57 ----- .../cyclomatic_complexity_rule_test.dart | 198 ++++++++++++++++++ 7 files changed, 362 insertions(+), 130 deletions(-) create mode 100644 lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart delete mode 100644 lint_test/cyclomatic_complexity_test.dart create mode 100644 test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index f2bab9aa..15fda505 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,8 @@ import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart'; import 'package:solid_lints/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart'; +import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart'; +import 'package:solid_lints/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart'; import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; @@ -42,6 +44,10 @@ class SolidLintsPlugin extends Plugin { analysisOptionsLoader: analysisLoader, parametersParser: AvoidReturningWidgetsParameters.fromJson, ), + CyclomaticComplexityRule( + analysisOptionsLoader: analysisLoader, + parametersParser: CyclomaticComplexityParameters.fromJson, + ), ]; for (final lintRule in lintRules) { diff --git a/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart b/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart index 75c36d76..b70e69f9 100644 --- a/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart +++ b/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart @@ -1,8 +1,8 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart'; -import 'package:solid_lints/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; +import 'package:solid_lints/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// Limit for the number of linearly independent paths through a program's @@ -17,54 +17,48 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// triggering a warning. /// /// ```yaml -/// custom_lint: -/// rules: -/// - cyclomatic_complexity: -/// max_complexity: 10 +/// plugins: +/// solid_lints: +/// diagnostics: +/// cyclomatic_complexity: +/// max_complexity: 10 /// ``` class CyclomaticComplexityRule extends SolidLintRule { - /// This lint rule represents the error if complexity - /// reaches maximum value. + /// Name of the lint. static const lintName = 'cyclomatic_complexity'; - CyclomaticComplexityRule._(super.rule); + static const _code = LintCode( + lintName, + 'The maximum allowed complexity of a function is {0}. Please decrease it.', + ); - /// Creates a new instance of [CyclomaticComplexityRule] - /// based on the lint configuration. - factory CyclomaticComplexityRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - paramsParser: CyclomaticComplexityParameters.fromJson, - problemMessage: (value) => - 'The maximum allowed complexity of a function is ' - '${value.maxComplexity}. Please decrease it.', - ); + @override + DiagnosticCode get diagnosticCode => _code; - return CyclomaticComplexityRule._(rule); - } + /// Creates a new instance of [CyclomaticComplexityRule]. + CyclomaticComplexityRule({ + required super.analysisOptionsLoader, + required super.parametersParser, + }) : super.withParameters( + name: _code.lowerCaseName, + description: + 'Limit for the number of linearly independent paths ' + "through a program's source code.", + ); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addBlockFunctionBody((node) { - context.registry.addDeclaration((declarationNode) { - final isIgnored = - config.parameters.exclude.shouldIgnore(declarationNode); - if (isIgnored) return; + super.registerNodeProcessors(registry, context); + + final parameters = getParametersForContext(context) ?? + CyclomaticComplexityParameters.empty(); - final visitor = CyclomaticComplexityFlowVisitor(); - node.visitChildren(visitor); + final visitor = CyclomaticComplexityVisitor(this, parameters); - if (visitor.complexityEntities.length + 1 > - config.parameters.maxComplexity) { - reporter.atNode(node, code); - } - }); - }); + registry.addCompilationUnit(this, visitor); } } diff --git a/lib/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart b/lib/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart index 93d3b2cd..ff5ddb59 100644 --- a/lib/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart +++ b/lib/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart @@ -17,6 +17,13 @@ class CyclomaticComplexityParameters { required this.exclude, }); + /// Empty [CyclomaticComplexityParameters] model. + factory CyclomaticComplexityParameters.empty() => + CyclomaticComplexityParameters( + maxComplexity: _defaultMaxComplexity, + exclude: ExcludedIdentifiersListParameter(exclude: []), + ); + /// Method for creating from json data factory CyclomaticComplexityParameters.fromJson(Map json) => CyclomaticComplexityParameters( diff --git a/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart index c101ced4..c29fa539 100644 --- a/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart +++ b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart @@ -31,14 +31,6 @@ import 'package:analyzer/dart/ast/visitor.dart'; /// The AST visitor that will collect cyclomatic complexity of visit nodes in an /// AST structure. class CyclomaticComplexityFlowVisitor extends RecursiveAstVisitor { - static const _complexityTokenTypes = [ - TokenType.AMPERSAND_AMPERSAND, - TokenType.BAR_BAR, - TokenType.QUESTION_PERIOD, - TokenType.QUESTION_QUESTION, - TokenType.QUESTION_QUESTION_EQ, - ]; - final _complexityEntities = {}; /// Returns an array of entities that increase cyclomatic complexity. @@ -52,13 +44,46 @@ class CyclomaticComplexityFlowVisitor extends RecursiveAstVisitor { } @override - void visitBlockFunctionBody(BlockFunctionBody node) { - _visitBlock( - node.block.leftBracket.next, - node.block.rightBracket, - ); + void visitBinaryExpression(BinaryExpression node) { + final type = node.operator.type; + if (type == TokenType.AMPERSAND_AMPERSAND || + type == TokenType.BAR_BAR || + type == TokenType.QUESTION_QUESTION) { + _increaseComplexity(node); + } + super.visitBinaryExpression(node); + } + + @override + void visitAssignmentExpression(AssignmentExpression node) { + if (node.operator.type == TokenType.QUESTION_QUESTION_EQ) { + _increaseComplexity(node); + } + super.visitAssignmentExpression(node); + } + + @override + void visitPropertyAccess(PropertyAccess node) { + if (node.operator.type == TokenType.QUESTION_PERIOD) { + _increaseComplexity(node); + } + super.visitPropertyAccess(node); + } + + @override + void visitMethodInvocation(MethodInvocation node) { + if (node.operator?.type == TokenType.QUESTION_PERIOD) { + _increaseComplexity(node); + } + super.visitMethodInvocation(node); + } - super.visitBlockFunctionBody(node); + @override + void visitIndexExpression(IndexExpression node) { + if (node.question != null) { + _increaseComplexity(node); + } + super.visitIndexExpression(node); } @override @@ -75,16 +100,6 @@ class CyclomaticComplexityFlowVisitor extends RecursiveAstVisitor { super.visitConditionalExpression(node); } - @override - void visitExpressionFunctionBody(ExpressionFunctionBody node) { - _visitBlock( - node.expression.beginToken.previous, - node.expression.endToken.next, - ); - - super.visitExpressionFunctionBody(node); - } - @override void visitForStatement(ForStatement node) { _increaseComplexity(node); @@ -113,6 +128,27 @@ class CyclomaticComplexityFlowVisitor extends RecursiveAstVisitor { super.visitSwitchDefault(node); } + @override + void visitSwitchExpressionCase(SwitchExpressionCase node) { + _increaseComplexity(node); + + super.visitSwitchExpressionCase(node); + } + + @override + void visitSwitchPatternCase(SwitchPatternCase node) { + _increaseComplexity(node); + + super.visitSwitchPatternCase(node); + } + + @override + void visitWhenClause(WhenClause node) { + _increaseComplexity(node); + + super.visitWhenClause(node); + } + @override void visitWhileStatement(WhileStatement node) { _increaseComplexity(node); @@ -127,15 +163,19 @@ class CyclomaticComplexityFlowVisitor extends RecursiveAstVisitor { super.visitYieldStatement(node); } - void _visitBlock(Token? firstToken, Token? lastToken) { - var token = firstToken; - while (token != lastToken && token != null) { - if (token.matchesAny(_complexityTokenTypes)) { - _increaseComplexity(token); - } + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + // Stop recursion into nested function declarations. + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + // Stop recursion into nested method declarations. + } - token = token.next; - } + @override + void visitFunctionExpression(FunctionExpression node) { + // Stop recursion into nested function expressions (closures). } void _increaseComplexity(SyntacticEntity entity) { diff --git a/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart new file mode 100644 index 00000000..b20f9824 --- /dev/null +++ b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart @@ -0,0 +1,44 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart'; +import 'package:solid_lints/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart'; +import 'package:solid_lints/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart'; + +/// The AST visitor that will check the cyclomatic complexity of +/// functions and methods. +class CyclomaticComplexityVisitor extends RecursiveAstVisitor { + final CyclomaticComplexityRule _rule; + final CyclomaticComplexityParameters _parameters; + + /// Creates a new instance of [CyclomaticComplexityVisitor]. + CyclomaticComplexityVisitor(this._rule, this._parameters); + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + super.visitFunctionDeclaration(node); + _checkFunctionBody(node, node.functionExpression.body); + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + super.visitMethodDeclaration(node); + _checkFunctionBody(node, node.body); + } + + void _checkFunctionBody(Declaration declaration, FunctionBody? body) { + if (body == null) return; + + final isIgnored = _parameters.exclude.shouldIgnore(declaration); + if (isIgnored) return; + + final visitor = CyclomaticComplexityFlowVisitor(); + body.accept(visitor); + + if (visitor.complexityEntities.length + 1 > _parameters.maxComplexity) { + _rule.reportAtNode( + body, + arguments: [_parameters.maxComplexity.toString()], + ); + } + } +} diff --git a/lint_test/cyclomatic_complexity_test.dart b/lint_test/cyclomatic_complexity_test.dart deleted file mode 100644 index 116e1fe0..00000000 --- a/lint_test/cyclomatic_complexity_test.dart +++ /dev/null @@ -1,57 +0,0 @@ -// ignore_for_file: literal_only_boolean_expressions, prefer_early_return -// ignore_for_file: no_empty_block, prefer_match_file_name - -/// Check complexity fail -/// -/// `cyclomatic_complexity_metric: max_complexity` -/// expect_lint: cyclomatic_complexity -void cyclomaticComplexity() { - if (true) { - if (true) { - if (true) { - if (true) {} - } - } - } -} - -class A { - /// expect_lint: cyclomatic_complexity - void cyclomaticComplexity() { - if (true) { - if (true) { - if (true) { - if (true) {} - } - } - } - } - - void simple() { - if (true) {} - } -} - -// no lint -void excludeMethod() { - if (true) { - if (true) { - if (true) { - if (true) {} - } - } - } -} - -class Exclude { - // no lint - void excludeMethod() { - if (true) { - if (true) { - if (true) { - if (true) {} - } - } - } - } -} diff --git a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart new file mode 100644 index 00000000..f2aa168e --- /dev/null +++ b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart @@ -0,0 +1,198 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart'; +import 'package:solid_lints/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(CyclomaticComplexityRuleTest); + }); +} + +@reflectiveTest +class CyclomaticComplexityRuleTest extends AnalysisRuleTest { + static const _mockAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + cyclomatic_complexity: + max_complexity: 4 + exclude: + - class_name: Exclude + method_name: excludeMethod + - method_name: excludeMethod + '''; + + @override + void setUp() { + rule = CyclomaticComplexityRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + parametersParser: CyclomaticComplexityParameters.fromJson, + ); + super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +$_mockAnalysisOptionsContent''', + ); + } + + @override + String get analysisRule => CyclomaticComplexityRule.lintName; + + Future test_reports_when_complexity_exceeds_threshold() async { + await assertDiagnostics( + r''' +void cyclomaticComplexity() { + if (true) { + if (true) { + if (true) { + if (true) {} + } + } + } +} +''', + [ + lint(28, 90), + ], + ); + } + + Future test_does_not_report_when_complexity_is_within_threshold() async { + await assertNoDiagnostics( + r''' +void simple() { + if (true) {} +} +''', + ); + } + + Future test_does_not_report_on_excluded_method_in_class() async { + await assertNoDiagnostics( + r''' +class Exclude { + void excludeMethod() { + if (true) { + if (true) { + if (true) { + if (true) {} + } + } + } + } +} +''', + ); + } + + Future test_does_not_report_on_excluded_top_level_function() async { + await assertNoDiagnostics( + r''' +void excludeMethod() { + if (true) { + if (true) { + if (true) { + if (true) {} + } + } + } +} +''', + ); + } + + Future test_does_not_report_on_nested_functions() async { + await assertNoDiagnostics( + r''' +void parentFunction() { + if (true) {} + + void nestedFunction() { + if (true) { + if (true) { + if (true) {} + } + } + } +} +''', + ); + } + + Future test_reports_when_complexity_exceeds_threshold_with_switch_expression() async { + await assertDiagnostics( + r''' +String test(int val) { + return switch (val) { + 1 => 'one', + 2 => 'two', + 3 => 'three', + _ => 'other', + }; +} +''', + [ + lint(21, 100), + ], + ); + } + + Future test_reports_when_complexity_exceeds_threshold_due_to_guard_clause() async { + await assertDiagnostics( + r''' +String test(int val) { + return switch (val) { + 1 when val > 0 => 'one', + 2 => 'two', + _ => 'other', + }; +} +''', + [ + lint(21, 95), + ], + ); + } + + Future test_reports_when_complexity_exceeds_threshold_with_switch_statement_and_patterns() async { + await assertDiagnostics( + r''' +void test(Object val) { + switch (val) { + case int x: + print('int'); + case String s: + print('string'); + case double d: + print('double'); + default: + print('other'); + } +} +''', + [ + lint(22, 179), + ], + ); + } + + Future test_reports_when_complexity_exceeds_threshold_due_to_logical_operators() async { + await assertDiagnostics( + r''' +void test(bool a, bool b, bool c, bool d) { + if (a && b && c && d) {} +} +''', + [ + lint(42, 30), + ], + ); + } +} From 19497d36387a5a4fa7e933f367ff57d41bed5f19 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Thu, 18 Jun 2026 16:50:17 +0300 Subject: [PATCH 2/9] test: add coverage for nested closures in cyclomatic complexity rule and remove obsolete issue link --- lib/analysis_options_test.yaml | 1 - .../cyclomatic_complexity_rule_test.dart | 29 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/analysis_options_test.yaml b/lib/analysis_options_test.yaml index 0acf73b8..5db9e6b7 100644 --- a/lib/analysis_options_test.yaml +++ b/lib/analysis_options_test.yaml @@ -12,7 +12,6 @@ custom_lint: # Since we're not using the source-lines-of-code rule, `main()` function in test can # have high cyclomatic complexity. # For rationale against splitting up `main()` in tests, see `source-lines-of-code` comments. -# Also, there is a bug in metric calculation: https://github.com/dart-code-checker/dart-code-metrics/issues/663 - cyclomatic_complexity: false # Late keyword is allowed in tests in order to enable the use of custom mocks and diff --git a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart index f2aa168e..c8bf9b92 100644 --- a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart +++ b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart @@ -195,4 +195,33 @@ void test(bool a, bool b, bool c, bool d) { ], ); } + + Future test_does_not_count_complexity_in_closures() async { + await assertNoDiagnostics( + r''' +void main() { + Calculator? calc; + group('a', () { + group('b', () { + group('c', () { + test('adds one to input values', () { + calc = Calculator(); + expect(calc?.addOne(2), 3); + expect(calc?.addOne(-7), -6); + expect(calc?.addOne(0), 1); + }); + }); + }); + }); +} +void group(String name, void Function() body) {} +void test(String name, void Function() body) {} +void expect(Object? actual, Object? matcher) {} +class Calculator { + int addOne(int value) => value + 1; } +''', + ); + } +} + From 8603c88e4abc35cd9717e39a4d495c5966a231b5 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Fri, 19 Jun 2026 11:46:44 +0300 Subject: [PATCH 3/9] fix: update cyclomatic complexity rule to support constructors and do-statements --- .../cyclomatic_complexity/cyclomatic_complexity_rule.dart | 2 +- .../visitors/cyclomatic_complexity_flow_visitor.dart | 7 +++++++ .../visitors/cyclomatic_complexity_visitor.dart | 6 ++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart b/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart index b70e69f9..0f1a687e 100644 --- a/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart +++ b/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart @@ -41,7 +41,7 @@ class CyclomaticComplexityRule required super.analysisOptionsLoader, required super.parametersParser, }) : super.withParameters( - name: _code.lowerCaseName, + name: lintName, description: 'Limit for the number of linearly independent paths ' "through a program's source code.", diff --git a/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart index c29fa539..2c951b77 100644 --- a/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart +++ b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart @@ -100,6 +100,13 @@ class CyclomaticComplexityFlowVisitor extends RecursiveAstVisitor { super.visitConditionalExpression(node); } + @override + void visitDoStatement(DoStatement node) { + _increaseComplexity(node); + + super.visitDoStatement(node); + } + @override void visitForStatement(ForStatement node) { _increaseComplexity(node); diff --git a/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart index b20f9824..5e88344f 100644 --- a/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart +++ b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart @@ -19,6 +19,12 @@ class CyclomaticComplexityVisitor extends RecursiveAstVisitor { _checkFunctionBody(node, node.functionExpression.body); } + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + super.visitConstructorDeclaration(node); + _checkFunctionBody(node, node.body); + } + @override void visitMethodDeclaration(MethodDeclaration node) { super.visitMethodDeclaration(node); From d9388575d499ad6d935bfe0fe3c43e7cacdfb29e Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Fri, 19 Jun 2026 11:47:08 +0300 Subject: [PATCH 4/9] refactor: migrate CyclomaticComplexityRuleTest to AutoTestLintOffsets and add additional test cases --- .../cyclomatic_complexity_rule_test.dart | 140 +++++++++--------- 1 file changed, 74 insertions(+), 66 deletions(-) diff --git a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart index c8bf9b92..afb9e793 100644 --- a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart +++ b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart @@ -5,6 +5,8 @@ import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexit import 'package:solid_lints/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; +import '../../../lints/auto_test_lint_offsets.dart'; + void main() { defineReflectiveSuite(() { defineReflectiveTests(CyclomaticComplexityRuleTest); @@ -12,7 +14,8 @@ void main() { } @reflectiveTest -class CyclomaticComplexityRuleTest extends AnalysisRuleTest { +class CyclomaticComplexityRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { static const _mockAnalysisOptionsContent = ''' plugins: solid_lints: @@ -46,9 +49,8 @@ $_mockAnalysisOptionsContent''', String get analysisRule => CyclomaticComplexityRule.lintName; Future test_reports_when_complexity_exceeds_threshold() async { - await assertDiagnostics( - r''' -void cyclomaticComplexity() { + await assertAutoDiagnostics(''' +void cyclomaticComplexity() ${expectLint(r'''{ if (true) { if (true) { if (true) { @@ -56,27 +58,20 @@ void cyclomaticComplexity() { } } } -} -''', - [ - lint(28, 90), - ], - ); +}''')} +'''); } Future test_does_not_report_when_complexity_is_within_threshold() async { - await assertNoDiagnostics( - r''' + await assertNoDiagnostics(r''' void simple() { if (true) {} } -''', - ); +'''); } Future test_does_not_report_on_excluded_method_in_class() async { - await assertNoDiagnostics( - r''' + await assertNoDiagnostics(r''' class Exclude { void excludeMethod() { if (true) { @@ -88,13 +83,11 @@ class Exclude { } } } -''', - ); +'''); } Future test_does_not_report_on_excluded_top_level_function() async { - await assertNoDiagnostics( - r''' + await assertNoDiagnostics(r''' void excludeMethod() { if (true) { if (true) { @@ -104,13 +97,11 @@ void excludeMethod() { } } } -''', - ); +'''); } Future test_does_not_report_on_nested_functions() async { - await assertNoDiagnostics( - r''' + await assertNoDiagnostics(r''' void parentFunction() { if (true) {} @@ -122,49 +113,37 @@ void parentFunction() { } } } -''', - ); +'''); } Future test_reports_when_complexity_exceeds_threshold_with_switch_expression() async { - await assertDiagnostics( - r''' -String test(int val) { + await assertAutoDiagnostics(''' +String test(int val) ${expectLint(r'''{ return switch (val) { 1 => 'one', 2 => 'two', 3 => 'three', _ => 'other', }; -} -''', - [ - lint(21, 100), - ], - ); +}''')} +'''); } Future test_reports_when_complexity_exceeds_threshold_due_to_guard_clause() async { - await assertDiagnostics( - r''' -String test(int val) { + await assertAutoDiagnostics(''' +String test(int val) ${expectLint(r'''{ return switch (val) { 1 when val > 0 => 'one', 2 => 'two', _ => 'other', }; -} -''', - [ - lint(21, 95), - ], - ); +}''')} +'''); } Future test_reports_when_complexity_exceeds_threshold_with_switch_statement_and_patterns() async { - await assertDiagnostics( - r''' -void test(Object val) { + await assertAutoDiagnostics(''' +void test(Object val) ${expectLint(r'''{ switch (val) { case int x: print('int'); @@ -175,30 +154,20 @@ void test(Object val) { default: print('other'); } -} -''', - [ - lint(22, 179), - ], - ); +}''')} +'''); } Future test_reports_when_complexity_exceeds_threshold_due_to_logical_operators() async { - await assertDiagnostics( - r''' -void test(bool a, bool b, bool c, bool d) { + await assertAutoDiagnostics(''' +void test(bool a, bool b, bool c, bool d) ${expectLint(r'''{ if (a && b && c && d) {} -} -''', - [ - lint(42, 30), - ], - ); +}''')} +'''); } Future test_does_not_count_complexity_in_closures() async { - await assertNoDiagnostics( - r''' + await assertNoDiagnostics(r''' void main() { Calculator? calc; group('a', () { @@ -220,8 +189,47 @@ void expect(Object? actual, Object? matcher) {} class Calculator { int addOne(int value) => value + 1; } -''', - ); +'''); + } + + Future test_reports_when_constructor_complexity_exceeds_threshold() async { + await assertAutoDiagnostics(''' +class Complex { + Complex(int val) ${expectLint(r'''{ + if (val > 0) { + if (val > 1) { + if (val > 2) { + if (val > 3) {} + } + } + } + }''')} +} +'''); + } + + Future test_does_not_report_on_simple_constructor() async { + await assertNoDiagnostics(r''' +class Simple { + Simple(int val) { + if (val > 0) {} } } +'''); + } + Future test_reports_when_complexity_exceeds_threshold_with_do_statement() async { + await assertAutoDiagnostics(''' +void testDoWhile() ${expectLint(r'''{ + int x = 0; + do { + if (x == 1) { + if (x == 2) { + if (x == 3) {} + } + } + } while (x < 10); +}''')} +'''); + } +} From 1c60fa506739acdb192f9126fbbe79648e712d67 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Fri, 19 Jun 2026 14:29:14 +0300 Subject: [PATCH 5/9] feat: include collection elements, logical patterns, and null-aware operators in cyclomatic complexity calculation --- .../cyclomatic_complexity_flow_visitor.dart | 40 +++++ .../cyclomatic_complexity_rule_test.dart | 145 ++++++++++++++++++ 2 files changed, 185 insertions(+) diff --git a/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart index 2c951b77..a9d026dc 100644 --- a/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart +++ b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart @@ -156,6 +156,46 @@ class CyclomaticComplexityFlowVisitor extends RecursiveAstVisitor { super.visitWhenClause(node); } + @override + void visitIfElement(IfElement node) { + _increaseComplexity(node); + super.visitIfElement(node); + } + + @override + void visitForElement(ForElement node) { + _increaseComplexity(node); + super.visitForElement(node); + } + + @override + void visitLogicalAndPattern(LogicalAndPattern node) { + _increaseComplexity(node); + super.visitLogicalAndPattern(node); + } + + @override + void visitLogicalOrPattern(LogicalOrPattern node) { + _increaseComplexity(node); + super.visitLogicalOrPattern(node); + } + + @override + void visitCascadeExpression(CascadeExpression node) { + if (node.isNullAware) { + _increaseComplexity(node); + } + super.visitCascadeExpression(node); + } + + @override + void visitSpreadElement(SpreadElement node) { + if (node.isNullAware) { + _increaseComplexity(node); + } + super.visitSpreadElement(node); + } + @override void visitWhileStatement(WhileStatement node) { _increaseComplexity(node); diff --git a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart index afb9e793..5284861b 100644 --- a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart +++ b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart @@ -230,6 +230,151 @@ void testDoWhile() ${expectLint(r'''{ } } while (x < 10); }''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_collection_if() async { + await assertAutoDiagnostics(''' +void test(bool a, bool b, bool c, bool d) ${expectLint(r'''{ + var result = [ + if (a) 1, + if (b) 2, + if (c) 3, + if (d) 4, + ]; +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_collection_for() async { + await assertAutoDiagnostics(''' +void test(List l1, List l2, List l3, List l4) ${expectLint(r'''{ + var result = [ + for (var x in l1) x, + for (var x in l2) x, + for (var x in l3) x, + for (var x in l4) x, + ]; +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_logical_and_pattern() async { + await assertAutoDiagnostics(''' +void test(Object val) ${expectLint(r'''{ + if (val case int x && > 0 && < 10 && != 5 && != 6) {} +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_logical_or_pattern() async { + await assertAutoDiagnostics(''' +void test(Object val) ${expectLint(r'''{ + if (val case int x || int x || int x || int x || int x) {} +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_null_aware_cascade() async { + await assertAutoDiagnostics(''' +void test(dynamic a, dynamic b, dynamic c, dynamic d) ${expectLint(r'''{ + a?..f(); + b?..f(); + c?..f(); + d?..f(); +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_null_aware_spread() async { + await assertAutoDiagnostics(''' +void test(List? a, List? b, List? c, List? d) ${expectLint(r'''{ + var r = [ + ...?a, + ...?b, + ...?c, + ...?d, + ]; +}''')} +'''); + } + + Future test_does_not_report_on_normal_cascades_and_spreads() async { + await assertNoDiagnostics(r''' +void test(dynamic x, List y) { + x..a()..b(); + [...y]; +} +'''); + } + + Future + test_does_not_report_when_collection_elements_within_threshold() async { + await assertNoDiagnostics(r''' +void test(bool a, List list) { + var result = [ + if (a) 1, + for (var x in list) x, + ]; +} +'''); + } + + Future test_does_not_double_count_cascade_sections() async { + await assertNoDiagnostics(r''' +void test(dynamic x) { + x?..a()..b()..c()..d()..e(); +} +'''); + } + + Future test_does_not_report_on_simple_pattern_matching() async { + await assertNoDiagnostics(r''' +void test(Object val) { + if (val case int x && > 0) {} +} +'''); + } + + Future + test_does_not_report_on_pattern_matching_or_within_threshold() async { + await assertNoDiagnostics(r''' +void test(Object val) { + if (val case int x || int x) {} +} +'''); + } + + Future test_does_not_count_collection_elements_in_closures() async { + await assertNoDiagnostics(r''' +void parent() { + final closure = () { + final list = [ + if (true) 1, + if (true) 2, + if (true) 3, + if (true) 4, + ]; + }; +} +'''); + } + + Future + test_does_not_count_null_aware_cascades_and_spreads_in_closures() async { + await assertNoDiagnostics(r''' +void parent() { + final closure = (dynamic x, List? y) { + x?..a()..b()..c()..d(); + [...?y]; + }; +} '''); } } From aac5683f138e99b89c9f5a894be3c691d0bac43b Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Fri, 19 Jun 2026 14:42:36 +0300 Subject: [PATCH 6/9] feat: include null check and null assert patterns in cyclomatic complexity analysis --- .../cyclomatic_complexity_flow_visitor.dart | 12 +++++++++ .../cyclomatic_complexity_rule_test.dart | 27 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart index a9d026dc..20114def 100644 --- a/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart +++ b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart @@ -180,6 +180,18 @@ class CyclomaticComplexityFlowVisitor extends RecursiveAstVisitor { super.visitLogicalOrPattern(node); } + @override + void visitNullCheckPattern(NullCheckPattern node) { + _increaseComplexity(node); + super.visitNullCheckPattern(node); + } + + @override + void visitNullAssertPattern(NullAssertPattern node) { + _increaseComplexity(node); + super.visitNullAssertPattern(node); + } + @override void visitCascadeExpression(CascadeExpression node) { if (node.isNullAware) { diff --git a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart index 5284861b..346186ee 100644 --- a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart +++ b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart @@ -375,6 +375,33 @@ void parent() { [...?y]; }; } +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_null_check_patterns() async { + await assertAutoDiagnostics(''' +void test(Object val) ${expectLint(r'''{ + if (val case [int? a?, int? b?, int? c?]) {} +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_null_assert_patterns() async { + await assertAutoDiagnostics(''' +void test(Object val) ${expectLint(r'''{ + if (val case [int? a!, int? b!, int? c!]) {} +}''')} +'''); + } + + Future + test_does_not_report_on_null_check_and_assert_patterns_within_threshold() async { + await assertNoDiagnostics(r''' +void test(Object val) { + if (val case [int? a?, int? b!]) {} +} '''); } } From 4ae15dcb093cfaa9baf20531df26855ff294ee85 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Fri, 19 Jun 2026 15:19:33 +0300 Subject: [PATCH 7/9] refactor: migrate cyclomatic complexity visitor to SimpleAstVisitor and target individual declaration types --- .../cyclomatic_complexity_rule.dart | 17 ++++---- .../cyclomatic_complexity_visitor.dart | 41 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart b/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart index 0f1a687e..c7024791 100644 --- a/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart +++ b/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart @@ -41,11 +41,11 @@ class CyclomaticComplexityRule required super.analysisOptionsLoader, required super.parametersParser, }) : super.withParameters( - name: lintName, - description: - 'Limit for the number of linearly independent paths ' - "through a program's source code.", - ); + name: lintName, + description: + 'Limit for the number of linearly independent paths ' + "through a program's source code.", + ); @override void registerNodeProcessors( @@ -54,11 +54,14 @@ class CyclomaticComplexityRule ) { super.registerNodeProcessors(registry, context); - final parameters = getParametersForContext(context) ?? + final parameters = + getParametersForContext(context) ?? CyclomaticComplexityParameters.empty(); final visitor = CyclomaticComplexityVisitor(this, parameters); - registry.addCompilationUnit(this, visitor); + registry.addFunctionDeclaration(this, visitor); + registry.addConstructorDeclaration(this, visitor); + registry.addMethodDeclaration(this, visitor); } } diff --git a/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart index 5e88344f..c3803997 100644 --- a/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart +++ b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart @@ -4,34 +4,16 @@ import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexit import 'package:solid_lints/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart'; import 'package:solid_lints/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart'; -/// The AST visitor that will check the cyclomatic complexity of -/// functions and methods. -class CyclomaticComplexityVisitor extends RecursiveAstVisitor { +/// Visitor that runs [CyclomaticComplexityFlowVisitor] on function, +/// constructor, and method bodies to count their cyclomatic complexity. +class CyclomaticComplexityVisitor extends SimpleAstVisitor { final CyclomaticComplexityRule _rule; final CyclomaticComplexityParameters _parameters; /// Creates a new instance of [CyclomaticComplexityVisitor]. CyclomaticComplexityVisitor(this._rule, this._parameters); - @override - void visitFunctionDeclaration(FunctionDeclaration node) { - super.visitFunctionDeclaration(node); - _checkFunctionBody(node, node.functionExpression.body); - } - - @override - void visitConstructorDeclaration(ConstructorDeclaration node) { - super.visitConstructorDeclaration(node); - _checkFunctionBody(node, node.body); - } - - @override - void visitMethodDeclaration(MethodDeclaration node) { - super.visitMethodDeclaration(node); - _checkFunctionBody(node, node.body); - } - - void _checkFunctionBody(Declaration declaration, FunctionBody? body) { + void _checkBody(Declaration declaration, FunctionBody? body) { if (body == null) return; final isIgnored = _parameters.exclude.shouldIgnore(declaration); @@ -47,4 +29,19 @@ class CyclomaticComplexityVisitor extends RecursiveAstVisitor { ); } } + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + _checkBody(node, node.functionExpression.body); + } + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + _checkBody(node, node.body); + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + _checkBody(node, node.body); + } } From d4d31f649dea33a4a7b641454586ebfe3aa70c2f Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Fri, 19 Jun 2026 15:35:21 +0300 Subject: [PATCH 8/9] chore: refactor test files formatting --- .../cyclomatic_complexity_rule_test.dart | 57 ++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart index 346186ee..e05f6a2a 100644 --- a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart +++ b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart @@ -62,7 +62,8 @@ void cyclomaticComplexity() ${expectLint(r'''{ '''); } - Future test_does_not_report_when_complexity_is_within_threshold() async { + Future + test_does_not_report_when_complexity_is_within_threshold() async { await assertNoDiagnostics(r''' void simple() { if (true) {} @@ -116,7 +117,8 @@ void parentFunction() { '''); } - Future test_reports_when_complexity_exceeds_threshold_with_switch_expression() async { + Future + test_reports_when_complexity_exceeds_threshold_with_switch_expression() async { await assertAutoDiagnostics(''' String test(int val) ${expectLint(r'''{ return switch (val) { @@ -129,7 +131,8 @@ String test(int val) ${expectLint(r'''{ '''); } - Future test_reports_when_complexity_exceeds_threshold_due_to_guard_clause() async { + Future + test_reports_when_complexity_exceeds_threshold_due_to_guard_clause() async { await assertAutoDiagnostics(''' String test(int val) ${expectLint(r'''{ return switch (val) { @@ -141,7 +144,8 @@ String test(int val) ${expectLint(r'''{ '''); } - Future test_reports_when_complexity_exceeds_threshold_with_switch_statement_and_patterns() async { + Future + test_reports_when_complexity_exceeds_threshold_with_switch_statement_and_patterns() async { await assertAutoDiagnostics(''' void test(Object val) ${expectLint(r'''{ switch (val) { @@ -158,7 +162,8 @@ void test(Object val) ${expectLint(r'''{ '''); } - Future test_reports_when_complexity_exceeds_threshold_due_to_logical_operators() async { + Future + test_reports_when_complexity_exceeds_threshold_due_to_logical_operators() async { await assertAutoDiagnostics(''' void test(bool a, bool b, bool c, bool d) ${expectLint(r'''{ if (a && b && c && d) {} @@ -192,7 +197,8 @@ class Calculator { '''); } - Future test_reports_when_constructor_complexity_exceeds_threshold() async { + Future + test_reports_when_constructor_complexity_exceeds_threshold() async { await assertAutoDiagnostics(''' class Complex { Complex(int val) ${expectLint(r'''{ @@ -218,7 +224,8 @@ class Simple { '''); } - Future test_reports_when_complexity_exceeds_threshold_with_do_statement() async { + Future + test_reports_when_complexity_exceeds_threshold_with_do_statement() async { await assertAutoDiagnostics(''' void testDoWhile() ${expectLint(r'''{ int x = 0; @@ -234,7 +241,7 @@ void testDoWhile() ${expectLint(r'''{ } Future - test_reports_when_complexity_exceeds_threshold_due_to_collection_if() async { + test_reports_when_complexity_exceeds_threshold_due_to_collection_if() async { await assertAutoDiagnostics(''' void test(bool a, bool b, bool c, bool d) ${expectLint(r'''{ var result = [ @@ -248,7 +255,7 @@ void test(bool a, bool b, bool c, bool d) ${expectLint(r'''{ } Future - test_reports_when_complexity_exceeds_threshold_due_to_collection_for() async { + test_reports_when_complexity_exceeds_threshold_due_to_collection_for() async { await assertAutoDiagnostics(''' void test(List l1, List l2, List l3, List l4) ${expectLint(r'''{ var result = [ @@ -262,7 +269,7 @@ void test(List l1, List l2, List l3, List l4) ${expectLint(r } Future - test_reports_when_complexity_exceeds_threshold_due_to_logical_and_pattern() async { + test_reports_when_complexity_exceeds_threshold_due_to_logical_and_pattern() async { await assertAutoDiagnostics(''' void test(Object val) ${expectLint(r'''{ if (val case int x && > 0 && < 10 && != 5 && != 6) {} @@ -271,7 +278,7 @@ void test(Object val) ${expectLint(r'''{ } Future - test_reports_when_complexity_exceeds_threshold_due_to_logical_or_pattern() async { + test_reports_when_complexity_exceeds_threshold_due_to_logical_or_pattern() async { await assertAutoDiagnostics(''' void test(Object val) ${expectLint(r'''{ if (val case int x || int x || int x || int x || int x) {} @@ -280,7 +287,7 @@ void test(Object val) ${expectLint(r'''{ } Future - test_reports_when_complexity_exceeds_threshold_due_to_null_aware_cascade() async { + test_reports_when_complexity_exceeds_threshold_due_to_null_aware_cascade() async { await assertAutoDiagnostics(''' void test(dynamic a, dynamic b, dynamic c, dynamic d) ${expectLint(r'''{ a?..f(); @@ -292,7 +299,7 @@ void test(dynamic a, dynamic b, dynamic c, dynamic d) ${expectLint(r'''{ } Future - test_reports_when_complexity_exceeds_threshold_due_to_null_aware_spread() async { + test_reports_when_complexity_exceeds_threshold_due_to_null_aware_spread() async { await assertAutoDiagnostics(''' void test(List? a, List? b, List? c, List? d) ${expectLint(r'''{ var r = [ @@ -315,7 +322,7 @@ void test(dynamic x, List y) { } Future - test_does_not_report_when_collection_elements_within_threshold() async { + test_does_not_report_when_collection_elements_within_threshold() async { await assertNoDiagnostics(r''' void test(bool a, List list) { var result = [ @@ -343,7 +350,7 @@ void test(Object val) { } Future - test_does_not_report_on_pattern_matching_or_within_threshold() async { + test_does_not_report_on_pattern_matching_or_within_threshold() async { await assertNoDiagnostics(r''' void test(Object val) { if (val case int x || int x) {} @@ -367,7 +374,7 @@ void parent() { } Future - test_does_not_count_null_aware_cascades_and_spreads_in_closures() async { + test_does_not_count_null_aware_cascades_and_spreads_in_closures() async { await assertNoDiagnostics(r''' void parent() { final closure = (dynamic x, List? y) { @@ -379,7 +386,19 @@ void parent() { } Future - test_reports_when_complexity_exceeds_threshold_due_to_null_check_patterns() async { + test_reports_when_complexity_exceeds_threshold_due_to_null_aware_function_calls() async { + await assertAutoDiagnostics(''' +void test(Function? a, Function? b, Function? c, Function? d) ${expectLint(r'''{ + a?.call(); + b?.call(); + c?.call(); + d?.call(); +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_null_check_patterns() async { await assertAutoDiagnostics(''' void test(Object val) ${expectLint(r'''{ if (val case [int? a?, int? b?, int? c?]) {} @@ -388,7 +407,7 @@ void test(Object val) ${expectLint(r'''{ } Future - test_reports_when_complexity_exceeds_threshold_due_to_null_assert_patterns() async { + test_reports_when_complexity_exceeds_threshold_due_to_null_assert_patterns() async { await assertAutoDiagnostics(''' void test(Object val) ${expectLint(r'''{ if (val case [int? a!, int? b!, int? c!]) {} @@ -397,7 +416,7 @@ void test(Object val) ${expectLint(r'''{ } Future - test_does_not_report_on_null_check_and_assert_patterns_within_threshold() async { + test_does_not_report_on_null_check_and_assert_patterns_within_threshold() async { await assertNoDiagnostics(r''' void test(Object val) { if (val case [int? a?, int? b!]) {} From cb9f15df2aa0509ba86863f33c1379b1b1a97eb3 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Fri, 19 Jun 2026 15:45:54 +0300 Subject: [PATCH 9/9] test: add cyclomatic complexity test case for nested for-in loops --- .../cyclomatic_complexity_rule_test.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart index e05f6a2a..86236e45 100644 --- a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart +++ b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart @@ -268,6 +268,22 @@ void test(List l1, List l2, List l3, List l4) ${expectLint(r '''); } + Future + test_reports_when_complexity_exceeds_threshold_due_to_for_in_loops() async { + await assertAutoDiagnostics(''' +void test(List l1, List l2, List l3, List l4) ${expectLint(r'''{ + for (var x in l1) { + for (var y in l2) { + for (var z in l3) { + for (var w in l4) {} + } + } + } +}''')} +'''); + } + + Future test_reports_when_complexity_exceeds_threshold_due_to_logical_and_pattern() async { await assertAutoDiagnostics('''