@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.
- package/dart_analyzer/README.md +226 -0
- package/dart_analyzer/analysis_options.yaml +66 -0
- package/dart_analyzer/bin/sunlint-dart-macos +0 -0
- package/dart_analyzer/bin/sunlint_dart_analyzer.dart +124 -0
- package/dart_analyzer/lib/analyzer_service.dart +625 -0
- package/dart_analyzer/lib/json_rpc_server.dart +275 -0
- package/dart_analyzer/lib/models/rule.dart +67 -0
- package/dart_analyzer/lib/models/symbol_table.dart +607 -0
- package/dart_analyzer/lib/models/violation.dart +69 -0
- package/dart_analyzer/lib/rules/base_analyzer.dart +52 -0
- package/dart_analyzer/lib/rules/common/C002_no_duplicate_code.dart +344 -0
- package/dart_analyzer/lib/rules/common/C003_no_vague_abbreviations.dart +318 -0
- package/dart_analyzer/lib/rules/common/C006_function_naming.dart +219 -0
- package/dart_analyzer/lib/rules/common/C008_variable_declaration_locality.dart +205 -0
- package/dart_analyzer/lib/rules/common/C010_limit_block_nesting.dart +162 -0
- package/dart_analyzer/lib/rules/common/C012_command_query_separation.dart +214 -0
- package/dart_analyzer/lib/rules/common/C013_no_dead_code.dart +225 -0
- package/dart_analyzer/lib/rules/common/C014_dependency_injection.dart +249 -0
- package/dart_analyzer/lib/rules/common/C017_constructor_logic.dart +158 -0
- package/dart_analyzer/lib/rules/common/C018_no_throw_generic_error.dart +141 -0
- package/dart_analyzer/lib/rules/common/C019_log_level_usage.dart +165 -0
- package/dart_analyzer/lib/rules/common/C020_unused_imports.dart +128 -0
- package/dart_analyzer/lib/rules/common/C021_import_organization.dart +86 -0
- package/dart_analyzer/lib/rules/common/C023_no_duplicate_variable.dart +112 -0
- package/dart_analyzer/lib/rules/common/C024_no_scatter_hardcoded_constants.dart +79 -0
- package/dart_analyzer/lib/rules/common/C029_catch_block_logging.dart +81 -0
- package/dart_analyzer/lib/rules/common/C030_use_custom_error_classes.dart +77 -0
- package/dart_analyzer/lib/rules/common/C031_validation_separation.dart +90 -0
- package/dart_analyzer/lib/rules/common/C033_separate_service_repository.dart +80 -0
- package/dart_analyzer/lib/rules/common/C035_error_logging_context.dart +148 -0
- package/dart_analyzer/lib/rules/common/C040_centralized_validation.dart +84 -0
- package/dart_analyzer/lib/rules/common/C041_no_sensitive_hardcode.dart +103 -0
- package/dart_analyzer/lib/rules/common/C042_boolean_name_prefix.dart +105 -0
- package/dart_analyzer/lib/rules/common/C043_no_console_or_print.dart +101 -0
- package/dart_analyzer/lib/rules/common/C047_no_duplicate_retry_logic.dart +94 -0
- package/dart_analyzer/lib/rules/common/C048_no_bypass_architectural_layers.dart +132 -0
- package/dart_analyzer/lib/rules/common/C052_parsing_or_data_transformation.dart +95 -0
- package/dart_analyzer/lib/rules/common/C060_no_override_superclass.dart +81 -0
- package/dart_analyzer/lib/rules/common/C065_one_behavior_per_test.dart +83 -0
- package/dart_analyzer/lib/rules/common/C067_no_hardcoded_config.dart +89 -0
- package/dart_analyzer/lib/rules/common/C070_no_real_time_tests.dart +99 -0
- package/dart_analyzer/lib/rules/common/C072_single_test_behavior.dart +78 -0
- package/dart_analyzer/lib/rules/common/C073_validate_required_config_on_startup.dart +82 -0
- package/dart_analyzer/lib/rules/common/C075_explicit_return_types.dart +85 -0
- package/dart_analyzer/lib/rules/common/C076_explicit_function_types.dart +104 -0
- package/dart_analyzer/lib/rules/dart/D001_recommended_lint_rules.dart +309 -0
- package/dart_analyzer/lib/rules/dart/D002_dispose_resources.dart +338 -0
- package/dart_analyzer/lib/rules/dart/D003_prefer_widgets_over_methods.dart +273 -0
- package/dart_analyzer/lib/rules/dart/D004_avoid_shrinkwrap_listview.dart +154 -0
- package/dart_analyzer/lib/rules/dart/D005_limit_widget_nesting.dart +265 -0
- package/dart_analyzer/lib/rules/dart/D006_prefer_extracting_large_callbacks.dart +135 -0
- package/dart_analyzer/lib/rules/dart/D007_prefer_init_first_dispose_last.dart +150 -0
- package/dart_analyzer/lib/rules/dart/D008_avoid_long_functions.dart +394 -0
- package/dart_analyzer/lib/rules/dart/D009_limit_function_parameters.dart +179 -0
- package/dart_analyzer/lib/rules/dart/D010_limit_cyclomatic_complexity.dart +257 -0
- package/dart_analyzer/lib/rules/dart/D011_prefer_named_parameters.dart +152 -0
- package/dart_analyzer/lib/rules/dart/D012_prefer_named_boolean_parameters.dart +156 -0
- package/dart_analyzer/lib/rules/dart/D013_single_public_class.dart +246 -0
- package/dart_analyzer/lib/rules/dart/D014_unsafe_collection_access.dart +202 -0
- package/dart_analyzer/lib/rules/dart/D015_copywith_all_parameters.dart +125 -0
- package/dart_analyzer/lib/rules/dart/D016_project_should_have_tests.dart +134 -0
- package/dart_analyzer/lib/rules/dart/D017_pubspec_dependencies_review.dart +187 -0
- package/dart_analyzer/lib/rules/dart/D018_remove_commented_code.dart +196 -0
- package/dart_analyzer/lib/rules/dart/D019_avoid_single_child_multi_child_widget.dart +161 -0
- package/dart_analyzer/lib/rules/dart/D020_limit_if_else_branches.dart +125 -0
- package/dart_analyzer/lib/rules/dart/D021_avoid_negated_boolean_checks.dart +227 -0
- package/dart_analyzer/lib/rules/dart/D022_use_setstate_correctly.dart +269 -0
- package/dart_analyzer/lib/rules/dart/D023_avoid_unnecessary_method_overrides.dart +191 -0
- package/dart_analyzer/lib/rules/dart/D024_avoid_unnecessary_stateful_widget.dart +194 -0
- package/dart_analyzer/lib/rules/dart/D025_avoid_nested_conditional_expressions.dart +90 -0
- package/dart_analyzer/lib/rules/security/S001_backend_auth_communications.dart +155 -0
- package/dart_analyzer/lib/rules/security/S002_os_command_injection.dart +159 -0
- package/dart_analyzer/lib/rules/security/S003_open_redirect_protection.dart +208 -0
- package/dart_analyzer/lib/rules/security/S004_sensitive_data_logging.dart +391 -0
- package/dart_analyzer/lib/rules/security/S005_trusted_service_authorization.dart +182 -0
- package/dart_analyzer/lib/rules/security/S006_no_default_credentials.dart +208 -0
- package/dart_analyzer/lib/rules/security/S007_output_encoding.dart +224 -0
- package/dart_analyzer/lib/rules/security/S008_svg_content_sanitization.dart +211 -0
- package/dart_analyzer/lib/rules/security/S009_no_insecure_encryption.dart +160 -0
- package/dart_analyzer/lib/rules/security/S010_use_csprng.dart +184 -0
- package/dart_analyzer/lib/rules/security/S011_ech_tls_config.dart +175 -0
- package/dart_analyzer/lib/rules/security/S012_hardcoded_secrets.dart +255 -0
- package/dart_analyzer/lib/rules/security/S013_tls_enforcement.dart +148 -0
- package/dart_analyzer/lib/rules/security/S014_tls_version_enforcement.dart +117 -0
- package/dart_analyzer/lib/rules/security/S015_insecure_tls_certificate.dart +315 -0
- package/dart_analyzer/lib/rules/security/S016_no_sensitive_querystring.dart +244 -0
- package/dart_analyzer/lib/rules/security/S017_use_parameterized_queries.dart +191 -0
- package/dart_analyzer/lib/rules/security/S018_no_sensitive_browser_storage.dart +175 -0
- package/dart_analyzer/lib/rules/security/S019_smtp_injection_protection.dart +166 -0
- package/dart_analyzer/lib/rules/security/S020_no_eval_dynamic_code.dart +149 -0
- package/dart_analyzer/lib/rules/security/S021_referrer_policy.dart +146 -0
- package/dart_analyzer/lib/rules/security/S022_escape_output_context.dart +111 -0
- package/dart_analyzer/lib/rules/security/S023_no_json_injection.dart +550 -0
- package/dart_analyzer/lib/rules/security/S024_xpath_xxe_protection.dart +299 -0
- package/dart_analyzer/lib/rules/security/S025_server_side_validation.dart +140 -0
- package/dart_analyzer/lib/rules/security/S026_tls_all_connections.dart +196 -0
- package/dart_analyzer/lib/rules/security/S027_mtls_certificate_validation.dart +195 -0
- package/dart_analyzer/lib/rules/security/S028_file_upload_size_limits.dart +186 -0
- package/dart_analyzer/lib/rules/security/S029_csrf_protection.dart +171 -0
- package/dart_analyzer/lib/rules/security/S030_directory_browsing_protection.dart +144 -0
- package/dart_analyzer/lib/rules/security/S031_secure_session_cookies.dart +118 -0
- package/dart_analyzer/lib/rules/security/S032_httponly_session_cookies.dart +114 -0
- package/dart_analyzer/lib/rules/security/S033_samesite_session_cookies.dart +120 -0
- package/dart_analyzer/lib/rules/security/S034_host_prefix_session_cookies.dart +160 -0
- package/dart_analyzer/lib/rules/security/S035_separate_app_hostnames.dart +117 -0
- package/dart_analyzer/lib/rules/security/S036_lfi_rfi_protection.dart +188 -0
- package/dart_analyzer/lib/rules/security/S037_cache_headers.dart +113 -0
- package/dart_analyzer/lib/rules/security/S038_no_version_headers.dart +114 -0
- package/dart_analyzer/lib/rules/security/S039_tls_certificate_validation.dart +131 -0
- package/dart_analyzer/lib/rules/security/S040_session_fixation_protection.dart +155 -0
- package/dart_analyzer/lib/rules/security/S041_session_token_invalidation.dart +201 -0
- package/dart_analyzer/lib/rules/security/S042_require_re_authentication_for_long_lived.dart +158 -0
- package/dart_analyzer/lib/rules/security/S043_password_changes_invalidate_all_sessions.dart +88 -0
- package/dart_analyzer/lib/rules/security/S044_re_authentication_required.dart +119 -0
- package/dart_analyzer/lib/rules/security/S045_brute_force_protection.dart +253 -0
- package/dart_analyzer/lib/rules/security/S046_jwt_algorithm_allowlist.dart +113 -0
- package/dart_analyzer/lib/rules/security/S047_oauth_pkce_protection.dart +124 -0
- package/dart_analyzer/lib/rules/security/S048_oauth_redirect_uri_validation.dart +134 -0
- package/dart_analyzer/lib/rules/security/S049_short_validity_tokens.dart +145 -0
- package/dart_analyzer/lib/rules/security/S050_reference_tokens_entropy.dart +234 -0
- package/dart_analyzer/lib/rules/security/S051_password_length_policy.dart +171 -0
- package/dart_analyzer/lib/rules/security/S052_weak_otp_entropy.dart +107 -0
- package/dart_analyzer/lib/rules/security/S053_generic_error_messages.dart +159 -0
- package/dart_analyzer/lib/rules/security/S054_no_default_accounts.dart +141 -0
- package/dart_analyzer/lib/rules/security/S055_content_type_validation.dart +324 -0
- package/dart_analyzer/lib/rules/security/S056_log_injection_protection.dart +119 -0
- package/dart_analyzer/lib/rules/security/S057_utc_logging.dart +114 -0
- package/dart_analyzer/lib/rules/security/S058_no_ssrf.dart +175 -0
- package/dart_analyzer/lib/rules/security/S059_disable_debug_mode.dart +172 -0
- package/dart_analyzer/lib/rules/security/S060_password_minimum_length.dart +170 -0
- package/dart_analyzer/lib/symbol_table_extractor.dart +510 -0
- package/dart_analyzer/lib/utils/common_utils.dart +26 -0
- package/dart_analyzer/pubspec.lock +557 -0
- package/dart_analyzer/pubspec.yaml +39 -0
- package/dart_analyzer/test/fixtures/complex_code.dart +95 -0
- package/docs/GENERATED_FILE_HANDLING_SUMMARY.md +2 -2
- 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
|
+
}
|