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..497931e9 --- /dev/null +++ b/lib/src/lints/function_lines_of_code/visitors/function_lines_of_code_rule_visitor.dart @@ -0,0 +1,74 @@ +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) { + 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..d6afbd3e --- /dev/null +++ b/test/src/lints/function_lines_of_code/function_lines_of_code_rule_test.dart @@ -0,0 +1,149 @@ +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/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 '../../../lints/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(FunctionLinesOfCodeRuleTest); + }); +} + +@reflectiveTest +class FunctionLinesOfCodeRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { + static const _mockAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + function_lines_of_code: + max_lines: 4 + exclude: + - class_name: ClassWithLongMethods + method_name: longMethodExcluded + - method_name: longMethodExcludedByDeclarationName + - method_name: longFunctionExcluded + - method_name: longFunctionExcludedByDeclarationName + - longMethodExcludedByString + '''; + + @override + void setUp() { + rule = FunctionLinesOfCodeRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + parametersParser: FunctionLinesOfCodeParameters.fromJson, + ); + super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +$_mockAnalysisOptionsContent''', + ); + } + + Future test_reports_when_lines_exceed_threshold() async { + await assertAutoDiagnostics(''' +${expectLint(r'''int longFunction() { + var i = 0; + i++; + i++; + i++; + + return i; +}''')} +'''); + } + + Future test_does_not_report_when_lines_within_threshold() async { + await assertNoDiagnostics(r''' +int shortFunction() { + var i = 0; + i++; + i++; + + return i; +} +'''); + } + + Future test_does_not_report_on_excluded_method() async { + await assertNoDiagnostics(r''' +class ClassWithLongMethods { + int longMethodExcluded() { + var i = 0; + i++; + i++; + i++; + + return i; + } +} +'''); + } + + Future test_does_not_report_on_excluded_top_level_function() async { + await assertNoDiagnostics(r''' +int longFunctionExcluded() { + var i = 0; + i++; + i++; + i++; + + return i; +} +'''); + } + + Future test_reports_on_anonymous_functions() async { + await assertAutoDiagnostics(''' +final longAnonymousFunction = ${expectLint(r'''() { + var i = 0; + i++; + i++; + i++; + + return i; +}''')}; +'''); + } + + Future test_does_not_report_on_method_excluded_by_string() async { + await assertNoDiagnostics(r''' +class SomeClass { + int longMethodExcludedByString() { + var i = 0; + i++; + i++; + i++; + + return i; + } +} +'''); + } + + Future + test_does_not_report_on_function_with_comments_and_blank_lines() async { + await assertNoDiagnostics(r''' +int shortFunctionWithComments() { + // This is a single-line comment. + var i = 0; + i++; + i++; + + /* + * This is a multi-line comment. + */ + + return i; +} +'''); + } +}