diff --git a/lib/main.dart b/lib/main.dart index 018d598f..6e7694bb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,6 +12,8 @@ import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_un import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/fixes/avoid_unnecessary_type_assertions_fix.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/function_lines_of_code/function_lines_of_code_rule.dart'; +import 'package:solid_lints/src/lints/function_lines_of_code/models/function_lines_of_code_parameters.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; /// The entry point for the Solid Lints analyser server plugin. @@ -47,6 +49,10 @@ class SolidLintsPlugin extends Plugin { analysisOptionsLoader: analysisLoader, parametersParser: AvoidReturningWidgetsParameters.fromJson, ), + FunctionLinesOfCodeRule( + analysisOptionsLoader: analysisLoader, + parametersParser: FunctionLinesOfCodeParameters.fromJson, + ), ]; for (final lintRule in lintRules) { diff --git a/lib/src/lints/function_lines_of_code/function_lines_of_code_rule.dart b/lib/src/lints/function_lines_of_code/function_lines_of_code_rule.dart index a8e7c89e..dd033c6d 100644 --- a/lib/src/lints/function_lines_of_code/function_lines_of_code_rule.dart +++ b/lib/src/lints/function_lines_of_code/function_lines_of_code_rule.dart @@ -1,9 +1,8 @@ -import 'package:analyzer/dart/ast/ast.dart'; -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/function_lines_of_code/models/function_lines_of_code_parameters.dart'; -import 'package:solid_lints/src/lints/function_lines_of_code/visitors/function_lines_of_code_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; +import 'package:solid_lints/src/lints/function_lines_of_code/visitors/function_lines_of_code_rule_visitor.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// An approximate metric of meaningful lines of source code inside a function, @@ -12,90 +11,52 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// ### Example config: /// /// ```yaml -/// custom_lint: -/// rules: -/// - function_lines_of_code: -/// max_lines: 100 -/// excludeNames: -/// - "Build" +/// plugins: +/// solid_lints: +/// diagnostics: +/// function_lines_of_code: +/// max_lines: 100 +/// exclude: +/// - "Build" /// ``` class FunctionLinesOfCodeRule extends SolidLintRule { - /// This lint rule represents the error if number of - /// parameters reaches the maximum value. + /// This lint rule name. static const lintName = 'function_lines_of_code'; - FunctionLinesOfCodeRule._(super.config); - - /// Creates a new instance of [FunctionLinesOfCodeRule] - /// based on the lint configuration. - factory FunctionLinesOfCodeRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - paramsParser: FunctionLinesOfCodeParameters.fromJson, - problemMessage: (value) => - 'The maximum allowed number of lines is ${value.maxLines}. ' - 'Try splitting this function into smaller parts.', - ); - - return FunctionLinesOfCodeRule._(rule); - } + static const _code = LintCode( + lintName, + 'The maximum allowed number of lines is {0}. ' + 'Try splitting this function into smaller parts.', + ); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, - ) { - void checkNode(AstNode node) => _checkNode(resolver, reporter, node); + DiagnosticCode get diagnosticCode => _code; - void checkDeclarationNode(Declaration node) { - final isIgnored = config.parameters.exclude.shouldIgnore(node); - if (isIgnored) { - return; - } - checkNode(node); - } - - // Check for an anonymous function - void checkFunctionExpressionNode(FunctionExpression node) { - // If a FunctionExpression is an immediate child of a FunctionDeclaration - // this means it's a named function, which are already check as part of - // addFunctionDeclaration call. - if (node.parent is FunctionDeclaration) { - return; - } - checkNode(node); - } - - context.registry.addFunctionDeclaration(checkDeclarationNode); - context.registry.addMethodDeclaration(checkDeclarationNode); - context.registry.addFunctionExpression(checkFunctionExpressionNode); - } + /// Creates a new instance of [FunctionLinesOfCodeRule] + FunctionLinesOfCodeRule({ + required super.analysisOptionsLoader, + required super.parametersParser, + }) : super.withParameters( + name: lintName, + description: + 'An approximate metric of meaningful lines of source code ' + 'inside a function, excluding blank lines and comments.', + ); - void _checkNode( - CustomLintResolver resolver, - DiagnosticReporter reporter, - AstNode node, + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - final visitor = FunctionLinesOfCodeVisitor(resolver.lineInfo); - node.visitChildren(visitor); + super.registerNodeProcessors(registry, context); - if (visitor.linesWithCode.length > config.parameters.maxLines) { - if (node is! AnnotatedNode) { - reporter.atNode(node, code); - return; - } + final parameters = + getParametersForContext(context) ?? + FunctionLinesOfCodeParameters.empty(); - final startOffset = node.firstTokenAfterCommentAndMetadata.offset; - final lengthDifference = startOffset - node.offset; + final visitor = FunctionLinesOfCodeRuleVisitor(this, context, parameters); - reporter.atOffset( - offset: startOffset, - length: node.length - lengthDifference, - diagnosticCode: code, - ); - } + registry.addCompilationUnit(this, visitor); } } diff --git a/lib/src/lints/function_lines_of_code/models/function_lines_of_code_parameters.dart b/lib/src/lints/function_lines_of_code/models/function_lines_of_code_parameters.dart index 7df6da2a..3e748c49 100644 --- a/lib/src/lints/function_lines_of_code/models/function_lines_of_code_parameters.dart +++ b/lib/src/lints/function_lines_of_code/models/function_lines_of_code_parameters.dart @@ -18,6 +18,14 @@ class FunctionLinesOfCodeParameters { required this.exclude, }); + /// Empty [FunctionLinesOfCodeParameters] model with default max lines. + factory FunctionLinesOfCodeParameters.empty() { + return FunctionLinesOfCodeParameters( + maxLines: _defaultMaxLines, + exclude: ExcludedIdentifiersListParameter(exclude: []), + ); + } + /// Method for creating from json data factory FunctionLinesOfCodeParameters.fromJson(Map json) => FunctionLinesOfCodeParameters( diff --git a/lib/src/lints/function_lines_of_code/visitors/function_lines_of_code_rule_visitor.dart b/lib/src/lints/function_lines_of_code/visitors/function_lines_of_code_rule_visitor.dart new file mode 100644 index 00000000..28e9eda8 --- /dev/null +++ b/lib/src/lints/function_lines_of_code/visitors/function_lines_of_code_rule_visitor.dart @@ -0,0 +1,73 @@ +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/function_lines_of_code/function_lines_of_code_rule.dart'; +import 'package:solid_lints/src/lints/function_lines_of_code/models/function_lines_of_code_parameters.dart'; +import 'package:solid_lints/src/lints/function_lines_of_code/visitors/function_lines_of_code_visitor.dart'; + +/// A visitor that reports on functions/methods exceeding the max line limit. +class FunctionLinesOfCodeRuleVisitor extends RecursiveAstVisitor { + final FunctionLinesOfCodeRule _rule; + final RuleContext _context; + final FunctionLinesOfCodeParameters _parameters; + + /// Creates a new instance of [FunctionLinesOfCodeRuleVisitor]. + FunctionLinesOfCodeRuleVisitor(this._rule, this._context, this._parameters); + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + final isIgnored = _parameters.exclude.shouldIgnore(node); + if (!isIgnored) { + _checkNode(node); + } + super.visitFunctionDeclaration(node); + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + final isIgnored = _parameters.exclude.shouldIgnore(node); + if (!isIgnored) { + _checkNode(node); + } + super.visitMethodDeclaration(node); + } + + @override + void visitFunctionExpression(FunctionExpression node) { + if (node.parent is! FunctionDeclaration) { + _checkNode(node); + } + super.visitFunctionExpression(node); + } + + void _checkNode(AstNode node) { + final currentUnit = _context.currentUnit; + if (currentUnit == null) return; + + final lineInfo = currentUnit.unit.lineInfo; + final visitor = FunctionLinesOfCodeVisitor(lineInfo); + node.visitChildren(visitor); + if (visitor.linesWithCode.length <= _parameters.maxLines) return; + + final reporter = currentUnit.diagnosticReporter; + if (node is! AnnotatedNode) { + reporter.atNode( + node, + _rule.diagnosticCode, + arguments: [_parameters.maxLines], + ); + + return; + } + + final startOffset = node.firstTokenAfterCommentAndMetadata.offset; + final lengthDifference = startOffset - node.offset; + + reporter.atOffset( + offset: startOffset, + length: node.length - lengthDifference, + diagnosticCode: _rule.diagnosticCode, + arguments: [_parameters.maxLines], + ); + } +} diff --git a/lint_test/function_lines_of_code_test/analysis_options.yaml b/lint_test/function_lines_of_code_test/analysis_options.yaml deleted file mode 100644 index 459a4d79..00000000 --- a/lint_test/function_lines_of_code_test/analysis_options.yaml +++ /dev/null @@ -1,14 +0,0 @@ -analyzer: - plugins: - - ../custom_lint - -custom_lint: - rules: - - function_lines_of_code: - max_lines: 5 - exclude: - - class_name: ClassWithLongMethods - method_name: longMethodExcluded - - method_name: longFunctionExcluded - - longFunctionExcludedByDeclarationName - - longMethodExcludedByDeclarationName diff --git a/lint_test/function_lines_of_code_test/function_lines_of_code_test.dart b/lint_test/function_lines_of_code_test/function_lines_of_code_test.dart deleted file mode 100644 index 032f5cd6..00000000 --- a/lint_test/function_lines_of_code_test/function_lines_of_code_test.dart +++ /dev/null @@ -1,535 +0,0 @@ -// ignore_for_file: prefer_match_file_name - -class ClassWithLongMethods { - // expect_lint: function_lines_of_code - int longMethod() { - var i = 0; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - - return i; - } - - // Excluded by method_name - int longMethodExcluded() { - var i = 0; - i++; - i++; - i++; - i++; - - return i; - } - -// Excluded by declaration_name - int longMethodExcludedByDeclarationName() { - var i = 0; - i++; - i++; - i++; - i++; - - return i; - } - - int notLongMethod() { - var i = 0; - i++; - i++; - i++; - - return i; - } -} - -int notLongFunction() { - var i = 0; - i++; - i++; - i++; - - return i; -} - -// expect_lint: function_lines_of_code -int longFunction() { - var i = 0; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - - return i; -} - -// Excluded by method_name -int longFunctionExcluded() { - var i = 0; - i++; - i++; - i++; - i++; - - return i; -} - -// Excluded by declaration_name -int longFunctionExcludedByDeclarationName() { - var i = 0; - i++; - i++; - i++; - i++; - - return i; -} - -// expect_lint: function_lines_of_code -final longAnonymousFunction = () { - var i = 0; - i++; - i++; - i++; - i++; - - return i; -}; - -final notLongAnonymousFunction = () { - var i = 0; - i++; - i++; - i++; - - return i; -}; diff --git a/test/src/lints/function_lines_of_code/function_lines_of_code_rule_test.dart b/test/src/lints/function_lines_of_code/function_lines_of_code_rule_test.dart new file mode 100644 index 00000000..8a9c4665 --- /dev/null +++ b/test/src/lints/function_lines_of_code/function_lines_of_code_rule_test.dart @@ -0,0 +1,138 @@ +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/function_lines_of_code/function_lines_of_code_rule.dart'; +import 'package:solid_lints/src/lints/function_lines_of_code/models/function_lines_of_code_parameters.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../utils/code_generators.dart'; +import '../../utils/table_driven_rule_test.dart'; +import 'models/test_case.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(FunctionLinesOfCodeRuleTest); + }); +} + +@reflectiveTest +class FunctionLinesOfCodeRuleTest extends TableDrivenRuleTest { + static const _excludedClassName = 'ExcludedClass'; + static const _nonExcludedClassName = 'NonExcludedClass'; + static const _excludedMethodName = 'excludedMethod'; + static const _excludedMethodByDeclaration = 'excludedMethodByDeclaration'; + static const _excludedFunctionName = 'excludedFunction'; + static const _excludedFunctionByDeclaration = 'excludedFunctionByDeclaration'; + static const _excludedByString = 'excludedByString'; + + static const _mockAnalysisOptionsContent = + ''' +plugins: + solid_lints: + diagnostics: + function_lines_of_code: + max_lines: 4 + exclude: + - class_name: $_excludedClassName + method_name: $_excludedMethodName + - method_name: $_excludedMethodByDeclaration + - method_name: $_excludedFunctionName + - method_name: $_excludedFunctionByDeclaration + - $_excludedByString + '''; + + /// All test cases for the function_lines_of_code rule. + static const testTable = { + // --- Threshold: fail when code lines > max (4) --- + TestCase(codeLines: 5): ExpectedResult.fail, + TestCase(codeLines: 5, comments: true): ExpectedResult.fail, + + // --- Threshold: pass when code lines ≤ max (4) --- + TestCase(codeLines: 4): ExpectedResult.pass, + TestCase(codeLines: 3): ExpectedResult.pass, + TestCase(codeLines: 4, comments: true): ExpectedResult.pass, + + // --- Exclude config --- + TestCase( + codeLines: 5, + className: _excludedClassName, + methodName: _excludedMethodName, + ): ExpectedResult.pass, + TestCase( + codeLines: 5, + className: _excludedClassName, + methodName: _excludedMethodByDeclaration, + ): ExpectedResult.pass, + TestCase(codeLines: 5, methodName: _excludedFunctionName): + ExpectedResult.pass, + TestCase(codeLines: 5, methodName: _excludedFunctionByDeclaration): + ExpectedResult.pass, + TestCase( + codeLines: 5, + className: _nonExcludedClassName, + methodName: _excludedByString, + ): ExpectedResult.pass, + + // --- Anonymous functions --- + TestCase(codeLines: 5, anonymous: true): ExpectedResult.fail, + }; + + @override + void setUp() { + rule = FunctionLinesOfCodeRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + parametersParser: FunctionLinesOfCodeParameters.fromJson, + ); + super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +$_mockAnalysisOptionsContent''', + ); + } + + /// Generates test source code for the given [testCase]. + /// + /// Returns the full [source] to analyze and the [lintTarget] substring + /// that should trigger the lint diagnostic (for fail cases). + @override + ({String source, String lintTarget}) generateCode(TestCase testCase) { + final indent = testCase.className != null ? ' ' : ' '; + final singleComment = testCase.comments + ? '$indent// This is a single-line comment.\n' + : ''; + final multiComment = testCase.comments + ? '\n$indent/*\n$indent * This is a multi-line comment.\n$indent */\n' + : ''; + final extra = testCase.codeLines - 2; + final stmts = extra > 0 + ? '\n${repeatLines('${indent}i++;', extra)}\n' + : '\n'; + final body = + '$singleComment${indent}var i = 0;$stmts$multiComment\n${indent}return i;\n'; + + if (testCase.anonymous) { + final fn = '() {\n$body}'; + return (source: 'final longAnonymousFunction = $fn;\n', lintTarget: fn); + } + + final name = testCase.methodName ?? 'function'; + + if (testCase.className != null) { + final method = ' int $name() {\n$body }'; + return ( + source: '\nclass ${testCase.className} {\n$method\n}\n', + lintTarget: method, + ); + } + + final fn = 'int $name() {\n$body}'; + return (source: '$fn\n', lintTarget: fn); + } + + Future test_function_lines_of_code_cases() async { + await runTableTests(testTable); + } +} diff --git a/test/src/lints/function_lines_of_code/models/test_case.dart b/test/src/lints/function_lines_of_code/models/test_case.dart new file mode 100644 index 00000000..49b36416 --- /dev/null +++ b/test/src/lints/function_lines_of_code/models/test_case.dart @@ -0,0 +1,38 @@ +/// Parameters for a single function_lines_of_code test case. +class TestCase { + /// Number of code lines inside the function body. + /// + /// `var i = 0;` and `return i;` are always present, so [codeLines] of 4 + /// means two extra `i++;` statements. + final int codeLines; + + /// Whether to include single-line and multi-line comments (not counted). + final bool comments; + + /// If set, wraps the function inside `class [className] { ... }`. + final String? className; + + /// Overrides the default function name (`function`). + final String? methodName; + + /// If true, generates an anonymous function literal instead. + final bool anonymous; + + const TestCase({ + required this.codeLines, + this.comments = false, + this.className, + this.methodName, + this.anonymous = false, + }); + + @override + String toString() { + final parts = ['codeLines: $codeLines']; + if (comments) parts.add('comments: true'); + if (className != null) parts.add('className: $className'); + if (methodName != null) parts.add('methodName: $methodName'); + if (anonymous) parts.add('anonymous: true'); + return '(${parts.join(', ')})'; + } +} diff --git a/test/src/utils/code_generators.dart b/test/src/utils/code_generators.dart new file mode 100644 index 00000000..d331ee4a --- /dev/null +++ b/test/src/utils/code_generators.dart @@ -0,0 +1,5 @@ +/// Repeats [string] [times] times, joining with newlines. +/// +/// Useful for generating test Dart code with a specific number of lines. +String repeatLines(String string, int times) => + List.generate(times, (_) => string).join('\n'); diff --git a/test/src/utils/table_driven_rule_test.dart b/test/src/utils/table_driven_rule_test.dart new file mode 100644 index 00000000..8f282f27 --- /dev/null +++ b/test/src/utils/table_driven_rule_test.dart @@ -0,0 +1,53 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:test/test.dart' hide setUp; + +import '../../lints/auto_test_lint_offsets.dart'; + +/// Result expected from a table-driven test case. +enum ExpectedResult { + /// The test case should pass without diagnostics. + pass, + + /// The test case should fail with a lint diagnostic. + fail, +} + +/// Base class for table-driven lint rule tests. +abstract class TableDrivenRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { + /// Disposes and recreates the analysis context. + Future resetAnalyzerContext() async { + // ignore: invalid_use_of_visible_for_testing_member + await super.tearDown(); + setUp(); + } + + /// Generates the test source code and the lint target for a given [testCase]. + ({String source, String lintTarget}) generateCode(T testCase); + + /// Executes all test cases defined in the [testTable] map. + Future runTableTests(Map testTable) async { + for (final MapEntry(key: testCase, value: expected) in testTable.entries) { + final (:source, :lintTarget) = generateCode(testCase); + + try { + switch (expected) { + case ExpectedResult.pass: + await assertNoDiagnostics(source); + case ExpectedResult.fail: + final marked = source.replaceFirst( + lintTarget, + expectLint(lintTarget), + ); + await assertAutoDiagnostics(marked); + } + } on TestFailure catch (e) { + fail('Case $testCase: $e'); + } + + // Reset the analysis context between test cases to bypass the analyzer's + // internal caching and ensure the newly generated code is re-analyzed. + await resetAnalyzerContext(); + } + } +}