@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,227 @@
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
+ /// D021: Avoid Negated Boolean Checks
10
+ /// Avoid inverted or negated boolean checks to improve code readability
11
+ class D021AvoidNegatedBooleanChecksAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'D021';
14
+
15
+ @override
16
+ List<Violation> analyze({
17
+ required CompilationUnit unit,
18
+ required String filePath,
19
+ required Rule rule,
20
+ required LineInfo lineInfo,
21
+ }) {
22
+ final violations = <Violation>[];
23
+
24
+ final visitor = _D021Visitor(
25
+ filePath: filePath,
26
+ lineInfo: lineInfo,
27
+ violations: violations,
28
+ analyzer: this,
29
+ );
30
+
31
+ unit.accept(visitor);
32
+
33
+ return violations;
34
+ }
35
+ }
36
+
37
+ class _D021Visitor extends RecursiveAstVisitor<void> {
38
+ final String filePath;
39
+ final LineInfo lineInfo;
40
+ final List<Violation> violations;
41
+ final D021AvoidNegatedBooleanChecksAnalyzer analyzer;
42
+
43
+ _D021Visitor({
44
+ required this.filePath,
45
+ required this.lineInfo,
46
+ required this.violations,
47
+ required this.analyzer,
48
+ });
49
+
50
+ @override
51
+ void visitIfStatement(IfStatement node) {
52
+ final condition = node.expression;
53
+
54
+ // Check for negated condition patterns
55
+ if (condition is PrefixExpression && condition.operator.lexeme == '!') {
56
+ final operand = condition.operand;
57
+
58
+ // Pattern 1: Double negation - if (!(!isValid))
59
+ if (operand is PrefixExpression && operand.operator.lexeme == '!') {
60
+ violations.add(analyzer.createViolation(
61
+ filePath: filePath,
62
+ line: analyzer.getLine(lineInfo, condition.offset),
63
+ column: analyzer.getColumn(lineInfo, condition.offset),
64
+ message: 'Avoid double negation. Use positive condition instead: if (${operand.operand})',
65
+ ));
66
+ }
67
+ // Pattern 2: Negated parenthesized expression - if (!(condition))
68
+ else if (operand is ParenthesizedExpression) {
69
+ final inner = operand.expression;
70
+
71
+ // Check for negated comparison that can use complementary operator
72
+ if (inner is BinaryExpression) {
73
+ final suggestion = _getComplementarySuggestion(inner);
74
+ if (suggestion != null) {
75
+ violations.add(analyzer.createViolation(
76
+ filePath: filePath,
77
+ line: analyzer.getLine(lineInfo, condition.offset),
78
+ column: analyzer.getColumn(lineInfo, condition.offset),
79
+ message: 'Use complementary operator instead: if ($suggestion)',
80
+ ));
81
+ }
82
+ }
83
+ // Negated compound condition - if (!(a && b))
84
+ else if (inner is BinaryExpression &&
85
+ (inner.operator.lexeme == '&&' || inner.operator.lexeme == '||')) {
86
+ violations.add(analyzer.createViolation(
87
+ filePath: filePath,
88
+ line: analyzer.getLine(lineInfo, condition.offset),
89
+ column: analyzer.getColumn(lineInfo, condition.offset),
90
+ message: 'Simplify negated compound condition using De Morgan\'s law. Instead of !(a && b), use !a || !b',
91
+ ));
92
+ }
93
+ }
94
+ // Pattern 3: Negated boolean variable/method with negative name - if (!isError)
95
+ // BUT: Skip state management patterns (isError, isLoading, etc. from state objects)
96
+ else if (operand is SimpleIdentifier || operand is MethodInvocation || operand is PrefixedIdentifier) {
97
+ final name = _getIdentifierName(operand);
98
+
99
+ // Check if this is a state property access (e.g., state.isError, asyncValue.isLoading)
100
+ final isStateProperty = operand is PrefixedIdentifier ||
101
+ (operand is PropertyAccess);
102
+
103
+ if (name != null && _hasNegativePrefix(name) && !_isStateManagementProperty(name, isStateProperty)) {
104
+ violations.add(analyzer.createViolation(
105
+ filePath: filePath,
106
+ line: analyzer.getLine(lineInfo, condition.offset),
107
+ column: analyzer.getColumn(lineInfo, condition.offset),
108
+ message: 'Replace negative with positive. Instead of !$name, consider using a positive boolean like is${_getPositiveCounterpart(name)}',
109
+ ));
110
+ }
111
+ }
112
+ }
113
+
114
+ super.visitIfStatement(node);
115
+ }
116
+
117
+ @override
118
+ void visitPrefixExpression(PrefixExpression node) {
119
+ // Check for negated boolean literals or identifiers outside of if statements
120
+ if (node.operator.lexeme == '!' && node.parent is! IfStatement) {
121
+ final operand = node.operand;
122
+
123
+ // Check for double negation in general expressions
124
+ if (operand is PrefixExpression && operand.operator.lexeme == '!') {
125
+ violations.add(analyzer.createViolation(
126
+ filePath: filePath,
127
+ line: analyzer.getLine(lineInfo, node.offset),
128
+ column: analyzer.getColumn(lineInfo, node.offset),
129
+ message: 'Avoid double negation: !!${operand.operand.toSource()}',
130
+ ));
131
+ }
132
+ }
133
+
134
+ super.visitPrefixExpression(node);
135
+ }
136
+
137
+ /// Get complementary operator suggestion for binary expressions
138
+ String? _getComplementarySuggestion(BinaryExpression expr) {
139
+ final left = expr.leftOperand.toSource();
140
+ final right = expr.rightOperand.toSource();
141
+ final op = expr.operator.lexeme;
142
+
143
+ switch (op) {
144
+ case '==':
145
+ return '$left != $right';
146
+ case '!=':
147
+ return '$left == $right';
148
+ case '>':
149
+ return '$left <= $right';
150
+ case '<':
151
+ return '$left >= $right';
152
+ case '>=':
153
+ return '$left < $right';
154
+ case '<=':
155
+ return '$left > $right';
156
+ default:
157
+ return null;
158
+ }
159
+ }
160
+
161
+ /// Get identifier name from various expression types
162
+ String? _getIdentifierName(Expression expr) {
163
+ if (expr is SimpleIdentifier) {
164
+ return expr.name;
165
+ } else if (expr is PrefixedIdentifier) {
166
+ return expr.identifier.name;
167
+ } else if (expr is MethodInvocation) {
168
+ return expr.methodName.name;
169
+ }
170
+ return null;
171
+ }
172
+
173
+ /// Check if identifier has negative prefix
174
+ bool _hasNegativePrefix(String name) {
175
+ return name.startsWith('isNot') ||
176
+ name.startsWith('isIn') && name.contains('valid') ||
177
+ name.startsWith('isError') ||
178
+ name.startsWith('isFail') ||
179
+ name.startsWith('isInvalid') ||
180
+ name.startsWith('isDisabled') ||
181
+ name.startsWith('isEmpty') ||
182
+ name.startsWith('isAbsent') ||
183
+ name.startsWith('isMissing') ||
184
+ name.startsWith('hasNo');
185
+ }
186
+
187
+ /// Check if this is a state management property that should not be flagged
188
+ /// State management patterns (Riverpod, BLoC, etc.) often have multiple states
189
+ /// where !isError doesn't mean isSuccess (could be isLoading)
190
+ bool _isStateManagementProperty(String name, bool isPropertyAccess) {
191
+ // If it's a property access (state.isError, asyncValue.isLoading), check if it's a common state property
192
+ if (isPropertyAccess) {
193
+ // Common state management properties that legitimately use multiple states
194
+ return name == 'isError' ||
195
+ name == 'isLoading' ||
196
+ name == 'isEmpty' || // List/collection state
197
+ name == 'isFailed' ||
198
+ name == 'isRefreshing' ||
199
+ name == 'isReloading';
200
+ }
201
+ return false;
202
+ }
203
+
204
+ /// Get positive counterpart suggestion
205
+ String _getPositiveCounterpart(String name) {
206
+ if (name.startsWith('isNot')) {
207
+ return name.substring(5); // Remove 'isNot'
208
+ } else if (name.startsWith('isError')) {
209
+ return 'Success';
210
+ } else if (name.startsWith('isFail')) {
211
+ return 'Success';
212
+ } else if (name.startsWith('isInvalid')) {
213
+ return 'Valid';
214
+ } else if (name.startsWith('isDisabled')) {
215
+ return 'Enabled';
216
+ } else if (name.startsWith('isEmpty')) {
217
+ return 'NotEmpty';
218
+ } else if (name.startsWith('isAbsent')) {
219
+ return 'Present';
220
+ } else if (name.startsWith('isMissing')) {
221
+ return 'Available';
222
+ } else if (name.startsWith('hasNo')) {
223
+ return 'Has${name.substring(5)}'; // Remove 'hasNo'
224
+ }
225
+ return 'Positive';
226
+ }
227
+ }
@@ -0,0 +1,269 @@
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
+ /// D022: Use setState Correctly
10
+ /// Ensure setState is used correctly in StatefulWidget without common anti-patterns
11
+ class D022UseSetStateCorrectlyAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'D022';
14
+
15
+ @override
16
+ List<Violation> analyze({
17
+ required CompilationUnit unit,
18
+ required String filePath,
19
+ required Rule rule,
20
+ required LineInfo lineInfo,
21
+ }) {
22
+ final violations = <Violation>[];
23
+
24
+ final visitor = _D022Visitor(
25
+ filePath: filePath,
26
+ lineInfo: lineInfo,
27
+ violations: violations,
28
+ analyzer: this,
29
+ );
30
+
31
+ unit.accept(visitor);
32
+
33
+ return violations;
34
+ }
35
+ }
36
+
37
+ class _D022Visitor extends RecursiveAstVisitor<void> {
38
+ final String filePath;
39
+ final LineInfo lineInfo;
40
+ final List<Violation> violations;
41
+ final D022UseSetStateCorrectlyAnalyzer analyzer;
42
+
43
+ // Track current method context
44
+ int _setStateDepth = 0;
45
+ final Map<int, List<MethodInvocation>> _setStateCallsByScope = {}; // Track setState nodes by scope
46
+ int _currentScopeId = 0;
47
+ final List<int> _scopeStack = []; // Stack of scope IDs
48
+
49
+ _D022Visitor({
50
+ required this.filePath,
51
+ required this.lineInfo,
52
+ required this.violations,
53
+ required this.analyzer,
54
+ });
55
+
56
+ @override
57
+ void visitMethodDeclaration(MethodDeclaration node) {
58
+ // Enter new scope for method body
59
+ _currentScopeId++;
60
+ final scopeId = _currentScopeId;
61
+ _scopeStack.add(scopeId);
62
+ _setStateCallsByScope[scopeId] = [];
63
+
64
+ super.visitMethodDeclaration(node);
65
+
66
+ // Exit scope - check for sequential setState calls
67
+ final setStateCalls = _setStateCallsByScope[scopeId] ?? [];
68
+ if (setStateCalls.length > 1) {
69
+ _checkSequentialSetStateCalls(setStateCalls);
70
+ }
71
+
72
+ _scopeStack.removeLast();
73
+ _setStateCallsByScope.remove(scopeId);
74
+ }
75
+
76
+ @override
77
+ void visitFunctionExpression(FunctionExpression node) {
78
+ // Enter new scope (callback function)
79
+ _currentScopeId++;
80
+ final scopeId = _currentScopeId;
81
+ _scopeStack.add(scopeId);
82
+ _setStateCallsByScope[scopeId] = [];
83
+
84
+ // When entering setState callback, check its body
85
+ if (node.parent is ArgumentList) {
86
+ final parent = node.parent as ArgumentList;
87
+ if (parent.parent is MethodInvocation) {
88
+ final methodInvocation = parent.parent as MethodInvocation;
89
+ if (methodInvocation.methodName.name == 'setState') {
90
+ // Check if setState callback contains async operations
91
+ _checkAsyncOperationsInSetState(node, methodInvocation);
92
+ }
93
+ }
94
+ }
95
+
96
+ super.visitFunctionExpression(node);
97
+
98
+ // Exit scope - check for sequential setState calls
99
+ final setStateCalls = _setStateCallsByScope[scopeId] ?? [];
100
+ if (setStateCalls.length > 1) {
101
+ // Check if setState calls are sequential (no control flow between them)
102
+ _checkSequentialSetStateCalls(setStateCalls);
103
+ }
104
+
105
+ _scopeStack.removeLast();
106
+ _setStateCallsByScope.remove(scopeId);
107
+ }
108
+
109
+ /// Check if setState calls are sequential (in a row, not in different conditionals)
110
+ void _checkSequentialSetStateCalls(List<MethodInvocation> setStateCalls) {
111
+ // Group setState calls by their immediate parent block
112
+ final Map<Block, List<MethodInvocation>> callsByBlock = {};
113
+
114
+ for (final call in setStateCalls) {
115
+ // Find the enclosing statement (ExpressionStatement)
116
+ AstNode? current = call;
117
+ while (current != null && current is! Statement) {
118
+ current = current.parent;
119
+ }
120
+
121
+ if (current == null) continue;
122
+
123
+ // Get the parent block
124
+ final blockParent = current.parent;
125
+ if (blockParent is! Block) continue;
126
+
127
+ // Check if this statement is inside a conditional branch
128
+ // by walking up and seeing if we hit a control flow before the block
129
+ bool isInConditionalBranch = false;
130
+ AstNode? node = current;
131
+
132
+ while (node != null && node != blockParent) {
133
+ final parent = node.parent;
134
+
135
+ // If the node is the body/then/else of a control structure, it's conditional
136
+ if (parent is IfStatement && (node == parent.thenStatement || node == parent.elseStatement)) {
137
+ isInConditionalBranch = true;
138
+ break;
139
+ }
140
+ if (parent is SwitchCase && node == parent.statements) {
141
+ isInConditionalBranch = true;
142
+ break;
143
+ }
144
+ if (parent is TryStatement && (node == parent.body || node == parent.finallyBlock)) {
145
+ isInConditionalBranch = true;
146
+ break;
147
+ }
148
+ if (parent is WhileStatement && node == parent.body) {
149
+ isInConditionalBranch = true;
150
+ break;
151
+ }
152
+ if (parent is ForStatement && node == parent.body) {
153
+ isInConditionalBranch = true;
154
+ break;
155
+ }
156
+ if (parent is DoStatement && node == parent.body) {
157
+ isInConditionalBranch = true;
158
+ break;
159
+ }
160
+
161
+ node = parent;
162
+ }
163
+
164
+ if (!isInConditionalBranch) {
165
+ callsByBlock.putIfAbsent(blockParent, () => []).add(call);
166
+ }
167
+ }
168
+
169
+ // Flag blocks that have 2+ sequential setState calls - report once with all line numbers
170
+ for (final entry in callsByBlock.entries) {
171
+ if (entry.value.length > 1) {
172
+ // Sort by line number
173
+ final sortedCalls = entry.value.toList()
174
+ ..sort((a, b) => a.offset.compareTo(b.offset));
175
+
176
+ // Get all line numbers
177
+ final lineNumbers = sortedCalls
178
+ .map((call) => analyzer.getLine(lineInfo, call.offset))
179
+ .join(', ');
180
+
181
+ // Report only at the first setState location with all line numbers
182
+ final firstCall = sortedCalls.first;
183
+ violations.add(analyzer.createViolation(
184
+ filePath: filePath,
185
+ line: analyzer.getLine(lineInfo, firstCall.offset),
186
+ column: analyzer.getColumn(lineInfo, firstCall.offset),
187
+ message: 'Multiple setState calls detected in sequence at lines $lineNumbers. Consider combining them into a single setState call for better performance.',
188
+ ));
189
+ }
190
+ }
191
+ }
192
+
193
+ @override
194
+ void visitMethodInvocation(MethodInvocation node) {
195
+ final methodName = node.methodName.name;
196
+
197
+ // Check for setState calls
198
+ if (methodName == 'setState') {
199
+ // Pattern: Nested setState (setState inside setState)
200
+ _setStateDepth++;
201
+ final isNestedSetState = _setStateDepth > 1;
202
+
203
+ if (isNestedSetState) {
204
+ violations.add(analyzer.createViolation(
205
+ filePath: filePath,
206
+ line: analyzer.getLine(lineInfo, node.offset),
207
+ column: analyzer.getColumn(lineInfo, node.offset),
208
+ message: 'Avoid nested setState calls. This can cause unnecessary rebuilds and makes state management harder to track.',
209
+ ));
210
+ } else {
211
+ // Only track top-level setState calls for multiple calls detection
212
+ // Track in current scope (not nested setState)
213
+ if (_scopeStack.isNotEmpty) {
214
+ final currentScope = _scopeStack.last;
215
+ _setStateCallsByScope[currentScope]?.add(node);
216
+ }
217
+ }
218
+
219
+ // Visit the callback to check for nested setState
220
+ super.visitMethodInvocation(node);
221
+
222
+ _setStateDepth--;
223
+ } else {
224
+ super.visitMethodInvocation(node);
225
+ }
226
+ }
227
+
228
+ /// Check for async operations inside setState callback
229
+ void _checkAsyncOperationsInSetState(FunctionExpression callback, MethodInvocation setStateCall) {
230
+ // Check if callback is async
231
+ if (callback.body is BlockFunctionBody) {
232
+ final body = callback.body as BlockFunctionBody;
233
+
234
+ // Check for async keyword
235
+ if (body.keyword?.lexeme == 'async') {
236
+ violations.add(analyzer.createViolation(
237
+ filePath: filePath,
238
+ line: analyzer.getLine(lineInfo, setStateCall.offset),
239
+ column: analyzer.getColumn(lineInfo, setStateCall.offset),
240
+ message: 'Avoid async callbacks in setState. setState should only update state synchronously. Perform async operations before calling setState.',
241
+ ));
242
+ }
243
+
244
+ // Check for await expressions in the body
245
+ final awaitChecker = _AwaitExpressionChecker();
246
+ body.block.accept(awaitChecker);
247
+
248
+ if (awaitChecker.hasAwait) {
249
+ violations.add(analyzer.createViolation(
250
+ filePath: filePath,
251
+ line: analyzer.getLine(lineInfo, setStateCall.offset),
252
+ column: analyzer.getColumn(lineInfo, setStateCall.offset),
253
+ message: 'setState callback contains await expression. Perform async operations outside setState and only update state synchronously.',
254
+ ));
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ /// Helper visitor to check for await expressions
261
+ class _AwaitExpressionChecker extends RecursiveAstVisitor<void> {
262
+ bool hasAwait = false;
263
+
264
+ @override
265
+ void visitAwaitExpression(AwaitExpression node) {
266
+ hasAwait = true;
267
+ super.visitAwaitExpression(node);
268
+ }
269
+ }
@@ -0,0 +1,191 @@
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
+ /// D023: Avoid Unnecessary Method Overrides
10
+ /// Methods that only call super with the same parameters are unnecessary
11
+ class D023AvoidUnnecessaryMethodOverridesAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'D023';
14
+
15
+ @override
16
+ List<Violation> analyze({
17
+ required CompilationUnit unit,
18
+ required String filePath,
19
+ required Rule rule,
20
+ required LineInfo lineInfo,
21
+ }) {
22
+ final violations = <Violation>[];
23
+
24
+ final visitor = _D023Visitor(
25
+ filePath: filePath,
26
+ lineInfo: lineInfo,
27
+ violations: violations,
28
+ analyzer: this,
29
+ );
30
+
31
+ unit.accept(visitor);
32
+
33
+ return violations;
34
+ }
35
+ }
36
+
37
+ class _D023Visitor extends RecursiveAstVisitor<void> {
38
+ final String filePath;
39
+ final LineInfo lineInfo;
40
+ final List<Violation> violations;
41
+ final D023AvoidUnnecessaryMethodOverridesAnalyzer analyzer;
42
+
43
+ _D023Visitor({
44
+ required this.filePath,
45
+ required this.lineInfo,
46
+ required this.violations,
47
+ required this.analyzer,
48
+ });
49
+
50
+ @override
51
+ void visitMethodDeclaration(MethodDeclaration node) {
52
+ // Check if method has @override annotation
53
+ final hasOverride = node.metadata.any((annotation) {
54
+ return annotation.name.name == 'override';
55
+ });
56
+
57
+ if (!hasOverride) {
58
+ super.visitMethodDeclaration(node);
59
+ return;
60
+ }
61
+
62
+ // Check if method body only contains a super call
63
+ final body = node.body;
64
+ if (body is! BlockFunctionBody) {
65
+ super.visitMethodDeclaration(node);
66
+ return;
67
+ }
68
+
69
+ final statements = body.block.statements;
70
+
71
+ // Method must have exactly one statement
72
+ if (statements.length != 1) {
73
+ super.visitMethodDeclaration(node);
74
+ return;
75
+ }
76
+
77
+ final statement = statements.first;
78
+ ExpressionStatement? expressionStatement;
79
+
80
+ // Handle both ExpressionStatement and ReturnStatement
81
+ if (statement is ExpressionStatement) {
82
+ expressionStatement = statement;
83
+ } else if (statement is ReturnStatement) {
84
+ // Check if return statement returns a super call
85
+ final returnExpression = statement.expression;
86
+ if (returnExpression is! MethodInvocation) {
87
+ super.visitMethodDeclaration(node);
88
+ return;
89
+ }
90
+
91
+ final methodInvocation = returnExpression;
92
+ if (!_isSuperCall(methodInvocation, node.name.lexeme)) {
93
+ super.visitMethodDeclaration(node);
94
+ return;
95
+ }
96
+
97
+ // Check if parameters match
98
+ if (_parametersMatch(node, methodInvocation)) {
99
+ violations.add(analyzer.createViolation(
100
+ filePath: filePath,
101
+ line: analyzer.getLine(lineInfo, node.name.offset),
102
+ column: analyzer.getColumn(lineInfo, node.name.offset),
103
+ message: 'Method "${node.name.lexeme}" only calls super.${node.name.lexeme}() with the same parameters. This override is unnecessary and can be removed.',
104
+ ));
105
+ }
106
+
107
+ super.visitMethodDeclaration(node);
108
+ return;
109
+ } else {
110
+ super.visitMethodDeclaration(node);
111
+ return;
112
+ }
113
+
114
+ // Get the expression from ExpressionStatement
115
+ final expression = expressionStatement.expression;
116
+ if (expression is! MethodInvocation) {
117
+ super.visitMethodDeclaration(node);
118
+ return;
119
+ }
120
+
121
+ final methodInvocation = expression;
122
+
123
+ // Check if it's a super call to the same method
124
+ if (!_isSuperCall(methodInvocation, node.name.lexeme)) {
125
+ super.visitMethodDeclaration(node);
126
+ return;
127
+ }
128
+
129
+ // Check if parameters match
130
+ if (_parametersMatch(node, methodInvocation)) {
131
+ violations.add(analyzer.createViolation(
132
+ filePath: filePath,
133
+ line: analyzer.getLine(lineInfo, node.name.offset),
134
+ column: analyzer.getColumn(lineInfo, node.name.offset),
135
+ message: 'Method "${node.name.lexeme}" only calls super.${node.name.lexeme}() with the same parameters. This override is unnecessary and can be removed.',
136
+ ));
137
+ }
138
+
139
+ super.visitMethodDeclaration(node);
140
+ }
141
+
142
+ /// Check if method invocation is a super call to the same method
143
+ bool _isSuperCall(MethodInvocation invocation, String methodName) {
144
+ final target = invocation.target;
145
+ if (target is! SuperExpression) return false;
146
+
147
+ return invocation.methodName.name == methodName;
148
+ }
149
+
150
+ /// Check if the parameters in the super call match the method parameters
151
+ bool _parametersMatch(MethodDeclaration method, MethodInvocation superCall) {
152
+ final methodParams = method.parameters?.parameters ?? [];
153
+ final superArgs = superCall.argumentList.arguments;
154
+
155
+ // Number of arguments must match
156
+ if (methodParams.length != superArgs.length) return false;
157
+
158
+ // Check each argument
159
+ for (var i = 0; i < methodParams.length; i++) {
160
+ final param = methodParams[i];
161
+ final arg = superArgs[i];
162
+
163
+ // Get parameter name
164
+ String? paramName;
165
+ if (param is SimpleFormalParameter) {
166
+ paramName = param.name?.lexeme;
167
+ } else if (param is DefaultFormalParameter) {
168
+ final innerParam = param.parameter;
169
+ if (innerParam is SimpleFormalParameter) {
170
+ paramName = innerParam.name?.lexeme;
171
+ }
172
+ }
173
+
174
+ // Check if argument is a simple identifier matching parameter name
175
+ if (arg is NamedExpression) {
176
+ // For named arguments, check both name and value
177
+ final argValue = arg.expression;
178
+ if (argValue is! SimpleIdentifier) return false;
179
+ if (argValue.name != paramName) return false;
180
+ } else if (arg is SimpleIdentifier) {
181
+ // For positional arguments, check if identifier matches parameter
182
+ if (arg.name != paramName) return false;
183
+ } else {
184
+ // Any other expression type means parameters don't simply match
185
+ return false;
186
+ }
187
+ }
188
+
189
+ return true;
190
+ }
191
+ }