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/lib/main.dart b/lib/main.dart index 708b9fce..34d3020f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,8 @@ import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_wi import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/fixes/avoid_unnecessary_type_assertions_fix.dart'; import 'package:solid_lints/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.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'; @@ -49,6 +51,10 @@ class SolidLintsPlugin extends Plugin { AvoidUnusedParametersRule( analysisOptionsLoader: analysisLoader, ), + 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..c7024791 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,51 @@ 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: lintName, + 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.addFunctionDeclaration(this, visitor); + registry.addConstructorDeclaration(this, visitor); + registry.addMethodDeclaration(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..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 @@ -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); + } - super.visitBlockFunctionBody(node); + @override + void visitMethodInvocation(MethodInvocation node) { + if (node.operator?.type == TokenType.QUESTION_PERIOD) { + _increaseComplexity(node); + } + super.visitMethodInvocation(node); + } + + @override + void visitIndexExpression(IndexExpression node) { + if (node.question != null) { + _increaseComplexity(node); + } + super.visitIndexExpression(node); } @override @@ -76,13 +101,10 @@ class CyclomaticComplexityFlowVisitor extends RecursiveAstVisitor { } @override - void visitExpressionFunctionBody(ExpressionFunctionBody node) { - _visitBlock( - node.expression.beginToken.previous, - node.expression.endToken.next, - ); + void visitDoStatement(DoStatement node) { + _increaseComplexity(node); - super.visitExpressionFunctionBody(node); + super.visitDoStatement(node); } @override @@ -113,6 +135,79 @@ 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 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 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) { + _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); @@ -127,15 +222,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. + } - token = token.next; - } + @override + void visitMethodDeclaration(MethodDeclaration node) { + // Stop recursion into nested method declarations. + } + + @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..c3803997 --- /dev/null +++ b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart @@ -0,0 +1,47 @@ +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'; + +/// 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); + + void _checkBody(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()], + ); + } + } + + @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); + } +} 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..86236e45 --- /dev/null +++ b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart @@ -0,0 +1,442 @@ +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'; + +import '../../../lints/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(CyclomaticComplexityRuleTest); + }); +} + +@reflectiveTest +class CyclomaticComplexityRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { + 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 assertAutoDiagnostics(''' +void cyclomaticComplexity() ${expectLint(r'''{ + if (true) { + if (true) { + if (true) { + if (true) {} + } + } + } +}''')} +'''); + } + + 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 assertAutoDiagnostics(''' +String test(int val) ${expectLint(r'''{ + return switch (val) { + 1 => 'one', + 2 => 'two', + 3 => 'three', + _ => 'other', + }; +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_guard_clause() async { + await assertAutoDiagnostics(''' +String test(int val) ${expectLint(r'''{ + return switch (val) { + 1 when val > 0 => 'one', + 2 => 'two', + _ => 'other', + }; +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_with_switch_statement_and_patterns() async { + await assertAutoDiagnostics(''' +void test(Object val) ${expectLint(r'''{ + switch (val) { + case int x: + print('int'); + case String s: + print('string'); + case double d: + print('double'); + default: + print('other'); + } +}''')} +'''); + } + + 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) {} +}''')} +'''); + } + + 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; +} +'''); + } + + 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); +}''')} +'''); + } + + 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_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(''' +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]; + }; +} +'''); + } + + Future + 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?]) {} +}''')} +'''); + } + + 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!]) {} +} +'''); + } +}