@sun-asterisk/sunlint 1.3.43 → 1.3.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/dart_analyzer/README.md +226 -0
  2. package/dart_analyzer/analysis_options.yaml +66 -0
  3. package/dart_analyzer/bin/sunlint-dart-macos +0 -0
  4. package/dart_analyzer/bin/sunlint_dart_analyzer.dart +124 -0
  5. package/dart_analyzer/lib/analyzer_service.dart +625 -0
  6. package/dart_analyzer/lib/json_rpc_server.dart +275 -0
  7. package/dart_analyzer/lib/models/rule.dart +67 -0
  8. package/dart_analyzer/lib/models/symbol_table.dart +607 -0
  9. package/dart_analyzer/lib/models/violation.dart +69 -0
  10. package/dart_analyzer/lib/rules/base_analyzer.dart +52 -0
  11. package/dart_analyzer/lib/rules/common/C002_no_duplicate_code.dart +344 -0
  12. package/dart_analyzer/lib/rules/common/C003_no_vague_abbreviations.dart +318 -0
  13. package/dart_analyzer/lib/rules/common/C006_function_naming.dart +219 -0
  14. package/dart_analyzer/lib/rules/common/C008_variable_declaration_locality.dart +205 -0
  15. package/dart_analyzer/lib/rules/common/C010_limit_block_nesting.dart +162 -0
  16. package/dart_analyzer/lib/rules/common/C012_command_query_separation.dart +214 -0
  17. package/dart_analyzer/lib/rules/common/C013_no_dead_code.dart +225 -0
  18. package/dart_analyzer/lib/rules/common/C014_dependency_injection.dart +249 -0
  19. package/dart_analyzer/lib/rules/common/C017_constructor_logic.dart +158 -0
  20. package/dart_analyzer/lib/rules/common/C018_no_throw_generic_error.dart +141 -0
  21. package/dart_analyzer/lib/rules/common/C019_log_level_usage.dart +165 -0
  22. package/dart_analyzer/lib/rules/common/C020_unused_imports.dart +128 -0
  23. package/dart_analyzer/lib/rules/common/C021_import_organization.dart +86 -0
  24. package/dart_analyzer/lib/rules/common/C023_no_duplicate_variable.dart +112 -0
  25. package/dart_analyzer/lib/rules/common/C024_no_scatter_hardcoded_constants.dart +79 -0
  26. package/dart_analyzer/lib/rules/common/C029_catch_block_logging.dart +81 -0
  27. package/dart_analyzer/lib/rules/common/C030_use_custom_error_classes.dart +77 -0
  28. package/dart_analyzer/lib/rules/common/C031_validation_separation.dart +90 -0
  29. package/dart_analyzer/lib/rules/common/C033_separate_service_repository.dart +80 -0
  30. package/dart_analyzer/lib/rules/common/C035_error_logging_context.dart +148 -0
  31. package/dart_analyzer/lib/rules/common/C040_centralized_validation.dart +84 -0
  32. package/dart_analyzer/lib/rules/common/C041_no_sensitive_hardcode.dart +103 -0
  33. package/dart_analyzer/lib/rules/common/C042_boolean_name_prefix.dart +105 -0
  34. package/dart_analyzer/lib/rules/common/C043_no_console_or_print.dart +101 -0
  35. package/dart_analyzer/lib/rules/common/C047_no_duplicate_retry_logic.dart +94 -0
  36. package/dart_analyzer/lib/rules/common/C048_no_bypass_architectural_layers.dart +132 -0
  37. package/dart_analyzer/lib/rules/common/C052_parsing_or_data_transformation.dart +95 -0
  38. package/dart_analyzer/lib/rules/common/C060_no_override_superclass.dart +81 -0
  39. package/dart_analyzer/lib/rules/common/C065_one_behavior_per_test.dart +83 -0
  40. package/dart_analyzer/lib/rules/common/C067_no_hardcoded_config.dart +89 -0
  41. package/dart_analyzer/lib/rules/common/C070_no_real_time_tests.dart +99 -0
  42. package/dart_analyzer/lib/rules/common/C072_single_test_behavior.dart +78 -0
  43. package/dart_analyzer/lib/rules/common/C073_validate_required_config_on_startup.dart +82 -0
  44. package/dart_analyzer/lib/rules/common/C075_explicit_return_types.dart +85 -0
  45. package/dart_analyzer/lib/rules/common/C076_explicit_function_types.dart +104 -0
  46. package/dart_analyzer/lib/rules/dart/D001_recommended_lint_rules.dart +309 -0
  47. package/dart_analyzer/lib/rules/dart/D002_dispose_resources.dart +338 -0
  48. package/dart_analyzer/lib/rules/dart/D003_prefer_widgets_over_methods.dart +273 -0
  49. package/dart_analyzer/lib/rules/dart/D004_avoid_shrinkwrap_listview.dart +154 -0
  50. package/dart_analyzer/lib/rules/dart/D005_limit_widget_nesting.dart +265 -0
  51. package/dart_analyzer/lib/rules/dart/D006_prefer_extracting_large_callbacks.dart +135 -0
  52. package/dart_analyzer/lib/rules/dart/D007_prefer_init_first_dispose_last.dart +150 -0
  53. package/dart_analyzer/lib/rules/dart/D008_avoid_long_functions.dart +394 -0
  54. package/dart_analyzer/lib/rules/dart/D009_limit_function_parameters.dart +179 -0
  55. package/dart_analyzer/lib/rules/dart/D010_limit_cyclomatic_complexity.dart +257 -0
  56. package/dart_analyzer/lib/rules/dart/D011_prefer_named_parameters.dart +152 -0
  57. package/dart_analyzer/lib/rules/dart/D012_prefer_named_boolean_parameters.dart +156 -0
  58. package/dart_analyzer/lib/rules/dart/D013_single_public_class.dart +246 -0
  59. package/dart_analyzer/lib/rules/dart/D014_unsafe_collection_access.dart +202 -0
  60. package/dart_analyzer/lib/rules/dart/D015_copywith_all_parameters.dart +125 -0
  61. package/dart_analyzer/lib/rules/dart/D016_project_should_have_tests.dart +134 -0
  62. package/dart_analyzer/lib/rules/dart/D017_pubspec_dependencies_review.dart +187 -0
  63. package/dart_analyzer/lib/rules/dart/D018_remove_commented_code.dart +196 -0
  64. package/dart_analyzer/lib/rules/dart/D019_avoid_single_child_multi_child_widget.dart +161 -0
  65. package/dart_analyzer/lib/rules/dart/D020_limit_if_else_branches.dart +125 -0
  66. package/dart_analyzer/lib/rules/dart/D021_avoid_negated_boolean_checks.dart +227 -0
  67. package/dart_analyzer/lib/rules/dart/D022_use_setstate_correctly.dart +269 -0
  68. package/dart_analyzer/lib/rules/dart/D023_avoid_unnecessary_method_overrides.dart +191 -0
  69. package/dart_analyzer/lib/rules/dart/D024_avoid_unnecessary_stateful_widget.dart +194 -0
  70. package/dart_analyzer/lib/rules/dart/D025_avoid_nested_conditional_expressions.dart +90 -0
  71. package/dart_analyzer/lib/rules/security/S001_backend_auth_communications.dart +155 -0
  72. package/dart_analyzer/lib/rules/security/S002_os_command_injection.dart +159 -0
  73. package/dart_analyzer/lib/rules/security/S003_open_redirect_protection.dart +208 -0
  74. package/dart_analyzer/lib/rules/security/S004_sensitive_data_logging.dart +391 -0
  75. package/dart_analyzer/lib/rules/security/S005_trusted_service_authorization.dart +182 -0
  76. package/dart_analyzer/lib/rules/security/S006_no_default_credentials.dart +208 -0
  77. package/dart_analyzer/lib/rules/security/S007_output_encoding.dart +224 -0
  78. package/dart_analyzer/lib/rules/security/S008_svg_content_sanitization.dart +211 -0
  79. package/dart_analyzer/lib/rules/security/S009_no_insecure_encryption.dart +160 -0
  80. package/dart_analyzer/lib/rules/security/S010_use_csprng.dart +184 -0
  81. package/dart_analyzer/lib/rules/security/S011_ech_tls_config.dart +175 -0
  82. package/dart_analyzer/lib/rules/security/S012_hardcoded_secrets.dart +255 -0
  83. package/dart_analyzer/lib/rules/security/S013_tls_enforcement.dart +148 -0
  84. package/dart_analyzer/lib/rules/security/S014_tls_version_enforcement.dart +117 -0
  85. package/dart_analyzer/lib/rules/security/S015_insecure_tls_certificate.dart +315 -0
  86. package/dart_analyzer/lib/rules/security/S016_no_sensitive_querystring.dart +244 -0
  87. package/dart_analyzer/lib/rules/security/S017_use_parameterized_queries.dart +191 -0
  88. package/dart_analyzer/lib/rules/security/S018_no_sensitive_browser_storage.dart +175 -0
  89. package/dart_analyzer/lib/rules/security/S019_smtp_injection_protection.dart +166 -0
  90. package/dart_analyzer/lib/rules/security/S020_no_eval_dynamic_code.dart +149 -0
  91. package/dart_analyzer/lib/rules/security/S021_referrer_policy.dart +146 -0
  92. package/dart_analyzer/lib/rules/security/S022_escape_output_context.dart +111 -0
  93. package/dart_analyzer/lib/rules/security/S023_no_json_injection.dart +550 -0
  94. package/dart_analyzer/lib/rules/security/S024_xpath_xxe_protection.dart +299 -0
  95. package/dart_analyzer/lib/rules/security/S025_server_side_validation.dart +140 -0
  96. package/dart_analyzer/lib/rules/security/S026_tls_all_connections.dart +196 -0
  97. package/dart_analyzer/lib/rules/security/S027_mtls_certificate_validation.dart +195 -0
  98. package/dart_analyzer/lib/rules/security/S028_file_upload_size_limits.dart +186 -0
  99. package/dart_analyzer/lib/rules/security/S029_csrf_protection.dart +171 -0
  100. package/dart_analyzer/lib/rules/security/S030_directory_browsing_protection.dart +144 -0
  101. package/dart_analyzer/lib/rules/security/S031_secure_session_cookies.dart +118 -0
  102. package/dart_analyzer/lib/rules/security/S032_httponly_session_cookies.dart +114 -0
  103. package/dart_analyzer/lib/rules/security/S033_samesite_session_cookies.dart +120 -0
  104. package/dart_analyzer/lib/rules/security/S034_host_prefix_session_cookies.dart +160 -0
  105. package/dart_analyzer/lib/rules/security/S035_separate_app_hostnames.dart +117 -0
  106. package/dart_analyzer/lib/rules/security/S036_lfi_rfi_protection.dart +188 -0
  107. package/dart_analyzer/lib/rules/security/S037_cache_headers.dart +113 -0
  108. package/dart_analyzer/lib/rules/security/S038_no_version_headers.dart +114 -0
  109. package/dart_analyzer/lib/rules/security/S039_tls_certificate_validation.dart +131 -0
  110. package/dart_analyzer/lib/rules/security/S040_session_fixation_protection.dart +155 -0
  111. package/dart_analyzer/lib/rules/security/S041_session_token_invalidation.dart +201 -0
  112. package/dart_analyzer/lib/rules/security/S042_require_re_authentication_for_long_lived.dart +158 -0
  113. package/dart_analyzer/lib/rules/security/S043_password_changes_invalidate_all_sessions.dart +88 -0
  114. package/dart_analyzer/lib/rules/security/S044_re_authentication_required.dart +119 -0
  115. package/dart_analyzer/lib/rules/security/S045_brute_force_protection.dart +253 -0
  116. package/dart_analyzer/lib/rules/security/S046_jwt_algorithm_allowlist.dart +113 -0
  117. package/dart_analyzer/lib/rules/security/S047_oauth_pkce_protection.dart +124 -0
  118. package/dart_analyzer/lib/rules/security/S048_oauth_redirect_uri_validation.dart +134 -0
  119. package/dart_analyzer/lib/rules/security/S049_short_validity_tokens.dart +145 -0
  120. package/dart_analyzer/lib/rules/security/S050_reference_tokens_entropy.dart +234 -0
  121. package/dart_analyzer/lib/rules/security/S051_password_length_policy.dart +171 -0
  122. package/dart_analyzer/lib/rules/security/S052_weak_otp_entropy.dart +107 -0
  123. package/dart_analyzer/lib/rules/security/S053_generic_error_messages.dart +159 -0
  124. package/dart_analyzer/lib/rules/security/S054_no_default_accounts.dart +141 -0
  125. package/dart_analyzer/lib/rules/security/S055_content_type_validation.dart +324 -0
  126. package/dart_analyzer/lib/rules/security/S056_log_injection_protection.dart +119 -0
  127. package/dart_analyzer/lib/rules/security/S057_utc_logging.dart +114 -0
  128. package/dart_analyzer/lib/rules/security/S058_no_ssrf.dart +175 -0
  129. package/dart_analyzer/lib/rules/security/S059_disable_debug_mode.dart +172 -0
  130. package/dart_analyzer/lib/rules/security/S060_password_minimum_length.dart +170 -0
  131. package/dart_analyzer/lib/symbol_table_extractor.dart +510 -0
  132. package/dart_analyzer/lib/utils/common_utils.dart +26 -0
  133. package/dart_analyzer/pubspec.lock +557 -0
  134. package/dart_analyzer/pubspec.yaml +39 -0
  135. package/dart_analyzer/test/fixtures/complex_code.dart +95 -0
  136. package/docs/GENERATED_FILE_HANDLING_SUMMARY.md +2 -2
  137. package/package.json +3 -2
@@ -0,0 +1,394 @@
1
+ import 'package:analyzer/dart/ast/ast.dart';
2
+ import 'package:analyzer/dart/ast/visitor.dart';
3
+ import 'package:analyzer/source/line_info.dart';
4
+
5
+ import '../../models/rule.dart';
6
+ import '../../models/violation.dart';
7
+ import '../base_analyzer.dart';
8
+
9
+ /// D008: Avoid Long Functions
10
+ /// Functions should not exceed the maximum line limit
11
+ class D008AvoidLongFunctionsAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'D008';
14
+
15
+ // Default maximum lines for a function
16
+ static const int _defaultMaxLines = 60;
17
+
18
+ @override
19
+ List<Violation> analyze({
20
+ required CompilationUnit unit,
21
+ required String filePath,
22
+ required Rule rule,
23
+ required LineInfo lineInfo,
24
+ }) {
25
+ final violations = <Violation>[];
26
+
27
+ // Get maxLines from rule config, default to 60
28
+ final maxLines = (rule.config['maxLines'] as int?) ?? _defaultMaxLines;
29
+
30
+ final visitor = _D008Visitor(
31
+ filePath: filePath,
32
+ lineInfo: lineInfo,
33
+ violations: violations,
34
+ analyzer: this,
35
+ maxLines: maxLines,
36
+ );
37
+
38
+ unit.accept(visitor);
39
+
40
+ return violations;
41
+ }
42
+ }
43
+
44
+ class _D008Visitor extends RecursiveAstVisitor<void> {
45
+ final String filePath;
46
+ final LineInfo lineInfo;
47
+ final List<Violation> violations;
48
+ final D008AvoidLongFunctionsAnalyzer analyzer;
49
+ final int maxLines;
50
+
51
+ _D008Visitor({
52
+ required this.filePath,
53
+ required this.lineInfo,
54
+ required this.violations,
55
+ required this.analyzer,
56
+ required this.maxLines,
57
+ });
58
+
59
+ @override
60
+ void visitFunctionDeclaration(FunctionDeclaration node) {
61
+ final isBuildMethod = false; // Functions can't be build method
62
+ final isWidgetBuilder = _isWidgetBuilder(node.returnType, node.functionExpression.body);
63
+
64
+ if (isWidgetBuilder) {
65
+ final logicLines = _countLogicLines(node.functionExpression.body);
66
+ if (logicLines <= 10) {
67
+ super.visitFunctionDeclaration(node);
68
+ return;
69
+ }
70
+ // Widget builder with too much logic - treat as long function
71
+ }
72
+
73
+ _checkFunctionLength(
74
+ node.functionExpression.body,
75
+ node.name.lexeme,
76
+ node.offset,
77
+ isBuildMethod: isBuildMethod,
78
+ isWidgetBuilder: isWidgetBuilder,
79
+ );
80
+ super.visitFunctionDeclaration(node);
81
+ }
82
+
83
+ @override
84
+ void visitMethodDeclaration(MethodDeclaration node) {
85
+ final isBuildMethod = node.name.lexeme == 'build';
86
+ final isWidgetBuilder = _isWidgetBuilder(node.returnType, node.body) || isBuildMethod;
87
+
88
+ if (isWidgetBuilder) {
89
+ final logicLines = _countLogicLines(node.body);
90
+ if (logicLines <= 10) {
91
+ super.visitMethodDeclaration(node);
92
+ return;
93
+ }
94
+ // Widget builder or build with too much logic - continue to check
95
+ }
96
+
97
+ _checkFunctionLength(
98
+ node.body,
99
+ node.name.lexeme,
100
+ node.offset,
101
+ isBuildMethod: isBuildMethod,
102
+ isWidgetBuilder: isWidgetBuilder,
103
+ );
104
+ super.visitMethodDeclaration(node);
105
+ }
106
+
107
+ /// Check if function/method is a widget builder
108
+ /// Return type contains "widget" (case insensitive) OR has widget tree pattern
109
+ bool _isWidgetBuilder(TypeAnnotation? returnType, FunctionBody body) {
110
+ final hasWidgetReturnType = returnType != null &&
111
+ returnType.toSource().toLowerCase().contains('widget');
112
+
113
+ final hasWidgetPattern = _hasNestedCapitalizedPattern(body);
114
+
115
+ return hasWidgetReturnType || hasWidgetPattern;
116
+ }
117
+
118
+ /// Count lines of logic code (excluding widget construction and comments)
119
+ int _countLogicLines(FunctionBody body) {
120
+ if (body is! BlockFunctionBody) {
121
+ return 0;
122
+ }
123
+
124
+ final block = body.block;
125
+ final statements = block.statements;
126
+
127
+ int logicLineCount = 0;
128
+
129
+ for (final statement in statements) {
130
+ // Skip return statements (usually widget construction)
131
+ if (statement is ReturnStatement) {
132
+ continue;
133
+ }
134
+
135
+ // Check if statement is primarily widget construction
136
+ if (_isWidgetConstructionStatement(statement)) {
137
+ continue; // Skip widget construction
138
+ }
139
+
140
+ // Count lines for this statement
141
+ final startLine = analyzer.getLine(lineInfo, statement.offset);
142
+ final endLine = analyzer.getLine(lineInfo, statement.end);
143
+ final statementLines = endLine - startLine + 1;
144
+
145
+ logicLineCount += statementLines;
146
+ }
147
+
148
+ return logicLineCount;
149
+ }
150
+
151
+ /// Check if a statement is primarily widget construction
152
+ bool _isWidgetConstructionStatement(Statement statement) {
153
+ // Skip if statements that only contain return statements (early returns)
154
+ if (statement is IfStatement) {
155
+ return _isEarlyReturnIfStatement(statement);
156
+ }
157
+
158
+ // Variable declarations that assign widgets
159
+ if (statement is VariableDeclarationStatement) {
160
+ final declarations = statement.variables.variables;
161
+ for (final decl in declarations) {
162
+ final initializer = decl.initializer;
163
+ if (initializer != null && _isWidgetExpression(initializer)) {
164
+ return true;
165
+ }
166
+ }
167
+ }
168
+
169
+ // Expression statements that are widget calls
170
+ if (statement is ExpressionStatement) {
171
+ return _isWidgetExpression(statement.expression);
172
+ }
173
+
174
+ return false;
175
+ }
176
+
177
+ /// Check if an if statement only contains return statements (early return pattern)
178
+ bool _isEarlyReturnIfStatement(IfStatement ifStatement) {
179
+ // Check the then branch
180
+ final thenStatement = ifStatement.thenStatement;
181
+ if (!_isReturnOrWidgetBlock(thenStatement)) {
182
+ return false;
183
+ }
184
+
185
+ // Check the else branch if it exists
186
+ final elseStatement = ifStatement.elseStatement;
187
+ if (elseStatement != null && !_isReturnOrWidgetBlock(elseStatement)) {
188
+ return false;
189
+ }
190
+
191
+ return true;
192
+ }
193
+
194
+ /// Check if a statement is a return statement or a block containing only returns
195
+ bool _isReturnOrWidgetBlock(Statement statement) {
196
+ if (statement is ReturnStatement) {
197
+ return true;
198
+ }
199
+
200
+ if (statement is Block) {
201
+ final statements = statement.statements;
202
+ // Empty block or all statements are returns
203
+ return statements.isEmpty ||
204
+ statements.every((s) => s is ReturnStatement);
205
+ }
206
+
207
+ return false;
208
+ }
209
+
210
+ /// Check if an expression is widget construction
211
+ bool _isWidgetExpression(Expression expression) {
212
+ if (expression is InstanceCreationExpression) {
213
+ final typeName = expression.constructorName.type.name2.lexeme;
214
+ if (typeName.isNotEmpty &&
215
+ _CapitalizedPatternVisitor._isCapitalized(typeName[0])) {
216
+ return true;
217
+ }
218
+ }
219
+
220
+ if (expression is MethodInvocation) {
221
+ final methodName = expression.methodName.name;
222
+ if (methodName.isNotEmpty &&
223
+ _CapitalizedPatternVisitor._isCapitalized(methodName[0])) {
224
+ return true;
225
+ }
226
+ }
227
+
228
+ return false;
229
+ }
230
+
231
+ /// Detect nested capitalized identifiers (Flutter widget tree pattern)
232
+ /// Example: Container(child: Column(children: [Text(...), Icon(...)]))
233
+ bool _hasNestedCapitalizedPattern(FunctionBody body) {
234
+ final visitor = _CapitalizedPatternVisitor();
235
+ body.accept(visitor);
236
+
237
+ // If we have multiple capitalized identifiers with nesting depth >= 2
238
+ // it's likely a widget tree
239
+ return visitor.capitalizedCount >= 3 && visitor.maxNestingDepth >= 2;
240
+ }
241
+
242
+ void _checkFunctionLength(
243
+ FunctionBody body,
244
+ String functionName,
245
+ int offset, {
246
+ required bool isBuildMethod,
247
+ required bool isWidgetBuilder,
248
+ }) {
249
+ final lineCount = _countEffectiveLines(body);
250
+
251
+ if (lineCount > maxLines) {
252
+ String message;
253
+
254
+ if (isBuildMethod) {
255
+ // Special message for build method with too much logic
256
+ message = 'Build method has too much logic ($lineCount lines, max $maxLines). Consider extracting logic into separate methods or widgets';
257
+ } else {
258
+ // Regular long function message
259
+ message = 'Function "$functionName" has $lineCount lines (max $maxLines). Consider breaking it into smaller functions';
260
+ }
261
+
262
+ violations.add(analyzer.createViolation(
263
+ filePath: filePath,
264
+ line: analyzer.getLine(lineInfo, offset),
265
+ column: analyzer.getColumn(lineInfo, offset),
266
+ message: message,
267
+ ));
268
+ }
269
+ }
270
+
271
+ int _countEffectiveLines(FunctionBody body) {
272
+ // For expression functions like: () => expr
273
+ if (body is ExpressionFunctionBody) {
274
+ final startLine = analyzer.getLine(lineInfo, body.expression.offset);
275
+ final endLine = analyzer.getLine(lineInfo, body.expression.end);
276
+ return endLine - startLine + 1;
277
+ }
278
+
279
+ // For block functions like: () { ... }
280
+ if (body is BlockFunctionBody) {
281
+ final block = body.block;
282
+ final statements = block.statements;
283
+
284
+ // Empty function
285
+ if (statements.isEmpty) {
286
+ return 0;
287
+ }
288
+
289
+ // Get first and last statement positions
290
+ final firstStatement = statements.first;
291
+ final lastStatement = statements.last;
292
+
293
+ final startLine = analyzer.getLine(lineInfo, firstStatement.offset);
294
+ final endLine = analyzer.getLine(lineInfo, lastStatement.end);
295
+
296
+ // Count lines from first statement to last statement (excluding opening/closing braces)
297
+ int totalLines = endLine - startLine + 1;
298
+
299
+ // Subtract comment lines
300
+ final commentLines = _countCommentLines(block, startLine, endLine);
301
+
302
+ return totalLines - commentLines;
303
+ }
304
+
305
+ // Fallback: count total lines
306
+ final startLine = analyzer.getLine(lineInfo, body.offset);
307
+ final endLine = analyzer.getLine(lineInfo, body.end);
308
+ return endLine - startLine + 1;
309
+ }
310
+
311
+ int _countCommentLines(Block block, int startLine, int endLine) {
312
+ // Get the source code of the block
313
+ final source = block.toSource();
314
+ final lines = source.split('\n');
315
+
316
+ int commentLineCount = 0;
317
+ bool inMultiLineComment = false;
318
+
319
+ for (final line in lines) {
320
+ final trimmed = line.trim();
321
+
322
+ // Check for multi-line comment start
323
+ if (trimmed.contains('/*')) {
324
+ inMultiLineComment = true;
325
+ }
326
+
327
+ // If in multi-line comment, count as comment line
328
+ if (inMultiLineComment) {
329
+ commentLineCount++;
330
+ // Check for multi-line comment end
331
+ if (trimmed.contains('*/')) {
332
+ inMultiLineComment = false;
333
+ }
334
+ continue;
335
+ }
336
+
337
+ // Check for single-line comment
338
+ if (trimmed.startsWith('//')) {
339
+ commentLineCount++;
340
+ continue;
341
+ }
342
+
343
+ // Check for inline comment (code + comment on same line)
344
+ // Don't count as full comment line since there's code
345
+ }
346
+
347
+ return commentLineCount;
348
+ }
349
+ }
350
+
351
+ /// Visitor to detect nested capitalized identifiers (Flutter widget pattern)
352
+ class _CapitalizedPatternVisitor extends RecursiveAstVisitor<void> {
353
+ int capitalizedCount = 0;
354
+ int maxNestingDepth = 0;
355
+ int _currentDepth = 0;
356
+
357
+ @override
358
+ void visitInstanceCreationExpression(InstanceCreationExpression node) {
359
+ final typeName = node.constructorName.type.name2.lexeme;
360
+
361
+ // Check if type name starts with uppercase (widget pattern)
362
+ if (typeName.isNotEmpty && _isCapitalized(typeName[0])) {
363
+ capitalizedCount++;
364
+ _currentDepth++;
365
+
366
+ if (_currentDepth > maxNestingDepth) {
367
+ maxNestingDepth = _currentDepth;
368
+ }
369
+
370
+ super.visitInstanceCreationExpression(node);
371
+
372
+ _currentDepth--;
373
+ return;
374
+ }
375
+
376
+ super.visitInstanceCreationExpression(node);
377
+ }
378
+
379
+ @override
380
+ void visitMethodInvocation(MethodInvocation node) {
381
+ final methodName = node.methodName.name;
382
+
383
+ // Check if method name starts with uppercase (could be a widget builder)
384
+ if (methodName.isNotEmpty && _isCapitalized(methodName[0])) {
385
+ capitalizedCount++;
386
+ }
387
+
388
+ super.visitMethodInvocation(node);
389
+ }
390
+
391
+ static bool _isCapitalized(String char) {
392
+ return char == char.toUpperCase() && char != char.toLowerCase();
393
+ }
394
+ }
@@ -0,0 +1,179 @@
1
+ import 'package:analyzer/dart/ast/ast.dart';
2
+ import 'package:analyzer/dart/ast/visitor.dart';
3
+ import 'package:analyzer/source/line_info.dart';
4
+
5
+ import '../../models/rule.dart';
6
+ import '../../models/violation.dart';
7
+ import '../base_analyzer.dart';
8
+
9
+ /// D009: Limit Function Parameters
10
+ /// Functions should not have too many parameters to maintain readability
11
+ class D009LimitFunctionParametersAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'D009';
14
+
15
+ // Default maximum number of parameters
16
+ static const int _defaultMaxParameters = 5;
17
+
18
+ // Default: skip nullable named parameters (they have implicit null default)
19
+ static const bool _defaultSkipNullableNamed = true;
20
+
21
+ @override
22
+ List<Violation> analyze({
23
+ required CompilationUnit unit,
24
+ required String filePath,
25
+ required Rule rule,
26
+ required LineInfo lineInfo,
27
+ }) {
28
+ final violations = <Violation>[];
29
+
30
+ // Get maxParameters from rule config, default to 5
31
+ final maxParameters =
32
+ (rule.config['maxParameters'] as int?) ?? _defaultMaxParameters;
33
+
34
+ // Get skipNullableNamedParameters from rule config, default to true
35
+ // Nullable named parameters have implicit null default, making them optional
36
+ final skipNullableNamed =
37
+ (rule.config['skipNullableNamedParameters'] as bool?) ??
38
+ _defaultSkipNullableNamed;
39
+
40
+ final visitor = _D009Visitor(
41
+ filePath: filePath,
42
+ lineInfo: lineInfo,
43
+ violations: violations,
44
+ analyzer: this,
45
+ maxParameters: maxParameters,
46
+ skipNullableNamed: skipNullableNamed,
47
+ );
48
+
49
+ unit.accept(visitor);
50
+
51
+ return violations;
52
+ }
53
+ }
54
+
55
+ class _D009Visitor extends RecursiveAstVisitor<void> {
56
+ final String filePath;
57
+ final LineInfo lineInfo;
58
+ final List<Violation> violations;
59
+ final D009LimitFunctionParametersAnalyzer analyzer;
60
+ final int maxParameters;
61
+ final bool skipNullableNamed;
62
+
63
+ _D009Visitor({
64
+ required this.filePath,
65
+ required this.lineInfo,
66
+ required this.violations,
67
+ required this.analyzer,
68
+ required this.maxParameters,
69
+ required this.skipNullableNamed,
70
+ });
71
+
72
+ @override
73
+ void visitFunctionDeclaration(FunctionDeclaration node) {
74
+ final parameters = node.functionExpression.parameters;
75
+ if (parameters != null) {
76
+ _checkParameterCount(
77
+ parameters,
78
+ node.name.lexeme,
79
+ 'Function',
80
+ );
81
+ }
82
+ super.visitFunctionDeclaration(node);
83
+ }
84
+
85
+ @override
86
+ void visitMethodDeclaration(MethodDeclaration node) {
87
+ final parameters = node.parameters;
88
+ if (parameters != null) {
89
+ _checkParameterCount(
90
+ parameters,
91
+ node.name.lexeme,
92
+ 'Method',
93
+ );
94
+ }
95
+ super.visitMethodDeclaration(node);
96
+ }
97
+
98
+ @override
99
+ void visitConstructorDeclaration(ConstructorDeclaration node) {
100
+ final parameters = node.parameters;
101
+ _checkParameterCount(
102
+ parameters,
103
+ node.name?.lexeme ?? '(unnamed constructor)',
104
+ 'Constructor',
105
+ );
106
+ super.visitConstructorDeclaration(node);
107
+ }
108
+
109
+ void _checkParameterCount(
110
+ FormalParameterList parameters,
111
+ String name,
112
+ String type,
113
+ ) {
114
+ // Count parameters, excluding optional ones based on configuration
115
+ int paramCount = 0;
116
+
117
+ for (final param in parameters.parameters) {
118
+ // Check if it's a DefaultFormalParameter (can be named or positional with defaults)
119
+ if (param is DefaultFormalParameter) {
120
+ final isNamed = param.isNamed;
121
+ final hasExplicitDefault = param.defaultValue != null;
122
+
123
+ // Skip named parameters with explicit default values
124
+ if (isNamed && hasExplicitDefault) {
125
+ continue;
126
+ }
127
+
128
+ // Skip nullable named parameters if configured (they have implicit null default)
129
+ if (skipNullableNamed && isNamed && !hasExplicitDefault) {
130
+ // Check if the parameter type is nullable
131
+ final paramType = param.parameter;
132
+ if (_isNullableParameter(paramType)) {
133
+ continue;
134
+ }
135
+ }
136
+ }
137
+
138
+ paramCount++;
139
+ }
140
+
141
+ if (paramCount > maxParameters) {
142
+ violations.add(analyzer.createViolation(
143
+ filePath: filePath,
144
+ line: analyzer.getLine(lineInfo, parameters.offset),
145
+ column: analyzer.getColumn(lineInfo, parameters.offset),
146
+ message:
147
+ '$type "$name" has $paramCount parameters, exceeding the limit of $maxParameters. Consider refactoring or adjusting maxParameters/skipNullableNamedParameters in config if needed.',
148
+ ));
149
+ }
150
+ }
151
+
152
+ /// Check if a parameter is nullable (has ? in type or uses dynamic/Object?)
153
+ bool _isNullableParameter(NormalFormalParameter param) {
154
+ if (param is SimpleFormalParameter) {
155
+ final type = param.type;
156
+ if (type == null) {
157
+ // No type annotation, could be dynamic
158
+ return false;
159
+ }
160
+
161
+ // Check if type has question mark (e.g., String?, int?)
162
+ final typeSource = type.toSource();
163
+ return typeSource.endsWith('?');
164
+ }
165
+
166
+ if (param is FieldFormalParameter) {
167
+ final type = param.type;
168
+ if (type == null) {
169
+ // Field formal parameter without explicit type
170
+ return false;
171
+ }
172
+
173
+ final typeSource = type.toSource();
174
+ return typeSource.endsWith('?');
175
+ }
176
+
177
+ return false;
178
+ }
179
+ }