diff --git a/lib/main.dart b/lib/main.dart index deb89a61..d9d338a8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,9 +11,9 @@ 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/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/function_lines_of_code/function_lines_of_code_rule.dart'; import 'package:solid_lints/src/lints/prefer_first/fixes/prefer_first_fix.dart'; import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; @@ -54,12 +54,14 @@ class SolidLintsPlugin extends Plugin { AvoidReturningWidgetsRule( analysisOptionsLoader: analysisLoader, ), + FunctionLinesOfCodeRule( + analysisOptionsLoader: analysisLoader, + ), AvoidUnusedParametersRule( analysisOptionsLoader: analysisLoader, ), CyclomaticComplexityRule( analysisOptionsLoader: analysisLoader, - parametersParser: CyclomaticComplexityParameters.fromJson, ), UseNearestContextRule(), preferFirstRule, diff --git a/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart b/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart index c7024791..79a6b671 100644 --- a/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart +++ b/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart @@ -39,12 +39,12 @@ class CyclomaticComplexityRule /// 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.", + parametersParser: CyclomaticComplexityParameters.fromJson, ); @override 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..49dc0d42 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, + }) : super.withParameters( + name: lintName, + description: + 'An approximate metric of meaningful lines of source code ' + 'inside a function, excluding blank lines and comments.', + parametersParser: FunctionLinesOfCodeParameters.fromJson, + ); - 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/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart b/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart index f780c3df..b5286057 100644 --- a/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart +++ b/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart @@ -2,7 +2,6 @@ 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/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:test_reflective_loader/test_reflective_loader.dart'; void main() { @@ -103,7 +102,6 @@ class BaseWidget extends StatelessWidget { analysisOptionsLoader: AnalysisOptionsLoader( resourceProvider: resourceProvider, ), - parametersParser: AvoidReturningWidgetsParameters.fromJson, ); newPackage('flutter') ..addFile('lib/widgets.dart', _mockFlutterWidgetsContent); 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 86236e45..4226997d 100644 --- a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart +++ b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart @@ -2,7 +2,6 @@ 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'; @@ -34,7 +33,6 @@ plugins: analysisOptionsLoader: AnalysisOptionsLoader( resourceProvider: resourceProvider, ), - parametersParser: CyclomaticComplexityParameters.fromJson, ); super.setUp(); 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..163f0988 --- /dev/null +++ b/test/src/lints/function_lines_of_code/function_lines_of_code_rule_test.dart @@ -0,0 +1,143 @@ +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: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, + ), + ); + super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +$_mockAnalysisOptionsContent''', + ); + } + + /// Generates test source code for the given [testCase] based on [expected]. + @override + String generateCode(TestCase testCase, ExpectedResult expected) { + final indent = testCase.className != null ? ' ' : ' '; + + final bodyBuffer = StringBuffer(); + if (testCase.comments) { + bodyBuffer.writeln('$indent// This is a single-line comment.'); + } + + bodyBuffer.writeln('${indent}var i = 0;'); + + final extra = testCase.codeLines - 2; + if (extra > 0) { + bodyBuffer.writeln('${indent}i++;'.repeatLines(extra)); + } + + if (testCase.comments) { + bodyBuffer.writeln('$indent/*'); + bodyBuffer.writeln('$indent * This is a multi-line comment.'); + bodyBuffer.writeln('$indent */'); + } + + bodyBuffer.write('${indent}return i;\n'); + final body = bodyBuffer.toString(); + + String wrap(String target) { + return expected == ExpectedResult.fail ? expectLint(target) : target; + } + + if (testCase.anonymous) { + final fn = '() {\n$body}'; + return 'final longAnonymousFunction = ${wrap(fn)};\n'; + } + + final name = testCase.methodName ?? 'function'; + + if (testCase.className != null) { + final method = ' int $name() {\n$body }'; + return '\nclass ${testCase.className} {\n${wrap(method)}\n}\n'; + } + + final fn = 'int $name() {\n$body}'; + return '${wrap(fn)}\n'; + } + + 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..5947a168 --- /dev/null +++ b/test/src/utils/code_generators.dart @@ -0,0 +1,8 @@ +/// Extension utilities for generating test code. +extension RepeatLinesExtension on String { + /// Repeats this string [times] times, joining with newlines. + /// + /// Useful for generating test Dart code with a specific number of lines. + String repeatLines(int times) => + List.generate(times, (_) => this).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..f27fdaad --- /dev/null +++ b/test/src/utils/table_driven_rule_test.dart @@ -0,0 +1,49 @@ +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 for a given [testCase] based on [expected]. + String generateCode(T testCase, ExpectedResult expected); + + /// 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 = generateCode(testCase, expected); + + try { + switch (expected) { + case ExpectedResult.pass: + await assertNoDiagnostics(source); + case ExpectedResult.fail: + await assertAutoDiagnostics(source); + } + } 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(); + } + } +}