@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,257 @@
|
|
|
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
|
+
/// D010: Limit Cyclomatic Complexity
|
|
10
|
+
/// Functions should not have high cyclomatic complexity to maintain readability
|
|
11
|
+
class D010LimitCyclomaticComplexityAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'D010';
|
|
14
|
+
|
|
15
|
+
// Default maximum cyclomatic complexity
|
|
16
|
+
static const int _defaultMaxComplexity = 10;
|
|
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 maxComplexity from rule config, default to 10
|
|
28
|
+
final maxComplexity = (rule.config['maxComplexity'] as int?) ?? _defaultMaxComplexity;
|
|
29
|
+
|
|
30
|
+
final visitor = _D010Visitor(
|
|
31
|
+
filePath: filePath,
|
|
32
|
+
lineInfo: lineInfo,
|
|
33
|
+
violations: violations,
|
|
34
|
+
analyzer: this,
|
|
35
|
+
maxComplexity: maxComplexity,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
unit.accept(visitor);
|
|
39
|
+
|
|
40
|
+
return violations;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class _D010Visitor extends RecursiveAstVisitor<void> {
|
|
45
|
+
final String filePath;
|
|
46
|
+
final LineInfo lineInfo;
|
|
47
|
+
final List<Violation> violations;
|
|
48
|
+
final D010LimitCyclomaticComplexityAnalyzer analyzer;
|
|
49
|
+
final int maxComplexity;
|
|
50
|
+
|
|
51
|
+
_D010Visitor({
|
|
52
|
+
required this.filePath,
|
|
53
|
+
required this.lineInfo,
|
|
54
|
+
required this.violations,
|
|
55
|
+
required this.analyzer,
|
|
56
|
+
required this.maxComplexity,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
@override
|
|
60
|
+
void visitFunctionDeclaration(FunctionDeclaration node) {
|
|
61
|
+
final complexity = _calculateComplexity(node.functionExpression.body);
|
|
62
|
+
if (complexity > maxComplexity) {
|
|
63
|
+
violations.add(analyzer.createViolation(
|
|
64
|
+
filePath: filePath,
|
|
65
|
+
line: analyzer.getLine(lineInfo, node.name.offset),
|
|
66
|
+
column: analyzer.getColumn(lineInfo, node.name.offset),
|
|
67
|
+
message: 'Function "${node.name.lexeme}" has cyclomatic complexity of $complexity, exceeding the limit of $maxComplexity',
|
|
68
|
+
));
|
|
69
|
+
}
|
|
70
|
+
super.visitFunctionDeclaration(node);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@override
|
|
74
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
75
|
+
final complexity = _calculateComplexity(node.body);
|
|
76
|
+
if (complexity > maxComplexity) {
|
|
77
|
+
violations.add(analyzer.createViolation(
|
|
78
|
+
filePath: filePath,
|
|
79
|
+
line: analyzer.getLine(lineInfo, node.name.offset),
|
|
80
|
+
column: analyzer.getColumn(lineInfo, node.name.offset),
|
|
81
|
+
message: 'Method "${node.name.lexeme}" has cyclomatic complexity of $complexity, exceeding the limit of $maxComplexity',
|
|
82
|
+
));
|
|
83
|
+
}
|
|
84
|
+
super.visitMethodDeclaration(node);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@override
|
|
88
|
+
void visitConstructorDeclaration(ConstructorDeclaration node) {
|
|
89
|
+
final complexity = _calculateComplexity(node.body);
|
|
90
|
+
if (complexity > maxComplexity) {
|
|
91
|
+
final name = node.name?.lexeme ?? '(unnamed constructor)';
|
|
92
|
+
violations.add(analyzer.createViolation(
|
|
93
|
+
filePath: filePath,
|
|
94
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
95
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
96
|
+
message: 'Constructor "$name" has cyclomatic complexity of $complexity, exceeding the limit of $maxComplexity',
|
|
97
|
+
));
|
|
98
|
+
}
|
|
99
|
+
super.visitConstructorDeclaration(node);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Calculate cyclomatic complexity for a function body
|
|
103
|
+
int _calculateComplexity(FunctionBody? body) {
|
|
104
|
+
if (body == null) return 1;
|
|
105
|
+
|
|
106
|
+
final complexityVisitor = _ComplexityVisitor();
|
|
107
|
+
body.accept(complexityVisitor);
|
|
108
|
+
|
|
109
|
+
// Cyclomatic complexity starts at 1 for the function itself
|
|
110
|
+
return 1 + complexityVisitor.complexity;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Visitor to calculate cyclomatic complexity
|
|
115
|
+
class _ComplexityVisitor extends RecursiveAstVisitor<void> {
|
|
116
|
+
int complexity = 0;
|
|
117
|
+
|
|
118
|
+
@override
|
|
119
|
+
void visitIfStatement(IfStatement node) {
|
|
120
|
+
// if statement adds 1
|
|
121
|
+
complexity++;
|
|
122
|
+
super.visitIfStatement(node);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@override
|
|
126
|
+
void visitConditionalExpression(ConditionalExpression node) {
|
|
127
|
+
// ternary operator (? :) adds 1
|
|
128
|
+
complexity++;
|
|
129
|
+
super.visitConditionalExpression(node);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@override
|
|
133
|
+
void visitForStatement(ForStatement node) {
|
|
134
|
+
// for loop adds 1
|
|
135
|
+
complexity++;
|
|
136
|
+
super.visitForStatement(node);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@override
|
|
140
|
+
void visitForElement(ForElement node) {
|
|
141
|
+
// for in collection adds 1
|
|
142
|
+
complexity++;
|
|
143
|
+
super.visitForElement(node);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@override
|
|
147
|
+
void visitWhileStatement(WhileStatement node) {
|
|
148
|
+
// while loop adds 1
|
|
149
|
+
complexity++;
|
|
150
|
+
super.visitWhileStatement(node);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@override
|
|
154
|
+
void visitDoStatement(DoStatement node) {
|
|
155
|
+
// do-while loop adds 1
|
|
156
|
+
complexity++;
|
|
157
|
+
super.visitDoStatement(node);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@override
|
|
161
|
+
void visitCatchClause(CatchClause node) {
|
|
162
|
+
// catch adds 1
|
|
163
|
+
complexity++;
|
|
164
|
+
super.visitCatchClause(node);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@override
|
|
168
|
+
void visitSwitchStatement(SwitchStatement node) {
|
|
169
|
+
// Each case adds 1 (except default or last case)
|
|
170
|
+
final members = node.members;
|
|
171
|
+
if (members.isNotEmpty) {
|
|
172
|
+
// Count all SwitchCase members except the last one
|
|
173
|
+
for (int i = 0; i < members.length - 1; i++) {
|
|
174
|
+
final member = members[i];
|
|
175
|
+
// Only count SwitchCase (not SwitchDefault)
|
|
176
|
+
if (member is SwitchCase || member is SwitchPatternCase) {
|
|
177
|
+
complexity++;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Don't count the last case (regardless of whether it's SwitchCase or SwitchDefault)
|
|
181
|
+
}
|
|
182
|
+
super.visitSwitchStatement(node);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@override
|
|
186
|
+
void visitSwitchExpression(SwitchExpression node) {
|
|
187
|
+
// Each case in switch expression adds 1 (except last)
|
|
188
|
+
final cases = node.cases;
|
|
189
|
+
if (cases.isNotEmpty) {
|
|
190
|
+
// All cases except the last one
|
|
191
|
+
complexity += cases.length - 1;
|
|
192
|
+
}
|
|
193
|
+
super.visitSwitchExpression(node);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@override
|
|
197
|
+
void visitBinaryExpression(BinaryExpression node) {
|
|
198
|
+
// && and || operators add 1
|
|
199
|
+
final operator = node.operator.lexeme;
|
|
200
|
+
if (operator == '&&' || operator == '||') {
|
|
201
|
+
complexity++;
|
|
202
|
+
}
|
|
203
|
+
super.visitBinaryExpression(node);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@override
|
|
207
|
+
void visitIfElement(IfElement node) {
|
|
208
|
+
// if in collection literal adds 1
|
|
209
|
+
complexity++;
|
|
210
|
+
super.visitIfElement(node);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@override
|
|
214
|
+
void visitPropertyAccess(PropertyAccess node) {
|
|
215
|
+
// Null-aware operator ?. adds 1
|
|
216
|
+
if (node.operator.lexeme == '?.') {
|
|
217
|
+
complexity++;
|
|
218
|
+
}
|
|
219
|
+
super.visitPropertyAccess(node);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@override
|
|
223
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
224
|
+
// Null-aware operator ?. adds 1
|
|
225
|
+
if (node.operator?.lexeme == '?.') {
|
|
226
|
+
complexity++;
|
|
227
|
+
}
|
|
228
|
+
super.visitMethodInvocation(node);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@override
|
|
232
|
+
void visitIndexExpression(IndexExpression node) {
|
|
233
|
+
// Null-aware operator ?[] adds 1
|
|
234
|
+
if (node.question != null) {
|
|
235
|
+
complexity++;
|
|
236
|
+
}
|
|
237
|
+
super.visitIndexExpression(node);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@override
|
|
241
|
+
void visitSpreadElement(SpreadElement node) {
|
|
242
|
+
// Null-aware spread ...? adds 1
|
|
243
|
+
if (node.spreadOperator.lexeme == '...?') {
|
|
244
|
+
complexity++;
|
|
245
|
+
}
|
|
246
|
+
super.visitSpreadElement(node);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@override
|
|
250
|
+
void visitAssignmentExpression(AssignmentExpression node) {
|
|
251
|
+
// ??= operator adds 1
|
|
252
|
+
if (node.operator.lexeme == '??=') {
|
|
253
|
+
complexity++;
|
|
254
|
+
}
|
|
255
|
+
super.visitAssignmentExpression(node);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
/// D011: Prefer Named Parameters
|
|
10
|
+
/// Functions with more than 3 parameters and adjacent parameters of the same type should use named parameters
|
|
11
|
+
class D011PreferNamedParametersAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'D011';
|
|
14
|
+
|
|
15
|
+
// Default minimum parameter count to trigger the check
|
|
16
|
+
static const int _defaultMinParameterCount = 3;
|
|
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 minParameterCount from rule config, default to 3
|
|
28
|
+
final minParameterCount = (rule.config['minParameterCount'] as int?) ?? _defaultMinParameterCount;
|
|
29
|
+
|
|
30
|
+
final visitor = _D011Visitor(
|
|
31
|
+
filePath: filePath,
|
|
32
|
+
lineInfo: lineInfo,
|
|
33
|
+
violations: violations,
|
|
34
|
+
analyzer: this,
|
|
35
|
+
minParameterCount: minParameterCount,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
unit.accept(visitor);
|
|
39
|
+
|
|
40
|
+
return violations;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class _D011Visitor extends RecursiveAstVisitor<void> {
|
|
45
|
+
final String filePath;
|
|
46
|
+
final LineInfo lineInfo;
|
|
47
|
+
final List<Violation> violations;
|
|
48
|
+
final D011PreferNamedParametersAnalyzer analyzer;
|
|
49
|
+
final int minParameterCount;
|
|
50
|
+
|
|
51
|
+
_D011Visitor({
|
|
52
|
+
required this.filePath,
|
|
53
|
+
required this.lineInfo,
|
|
54
|
+
required this.violations,
|
|
55
|
+
required this.analyzer,
|
|
56
|
+
required this.minParameterCount,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
@override
|
|
60
|
+
void visitFunctionDeclaration(FunctionDeclaration node) {
|
|
61
|
+
_checkParameters(
|
|
62
|
+
node.functionExpression.parameters,
|
|
63
|
+
node.name.lexeme,
|
|
64
|
+
node.name.offset,
|
|
65
|
+
);
|
|
66
|
+
super.visitFunctionDeclaration(node);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@override
|
|
70
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
71
|
+
_checkParameters(
|
|
72
|
+
node.parameters,
|
|
73
|
+
node.name.lexeme,
|
|
74
|
+
node.name.offset,
|
|
75
|
+
);
|
|
76
|
+
super.visitMethodDeclaration(node);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@override
|
|
80
|
+
void visitConstructorDeclaration(ConstructorDeclaration node) {
|
|
81
|
+
final name = node.name?.lexeme ?? '(unnamed constructor)';
|
|
82
|
+
_checkParameters(
|
|
83
|
+
node.parameters,
|
|
84
|
+
name,
|
|
85
|
+
node.offset,
|
|
86
|
+
);
|
|
87
|
+
super.visitConstructorDeclaration(node);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
void _checkParameters(FormalParameterList? parameters, String name, int offset) {
|
|
91
|
+
if (parameters == null) return;
|
|
92
|
+
|
|
93
|
+
final allParams = parameters.parameters;
|
|
94
|
+
if (allParams.length <= minParameterCount) return;
|
|
95
|
+
|
|
96
|
+
// Check if there are already named parameters
|
|
97
|
+
final hasNamedParams = allParams.any((p) => p.isNamed);
|
|
98
|
+
|
|
99
|
+
// Get only positional parameters for checking
|
|
100
|
+
final positionalParams = allParams.where((p) => !p.isNamed).toList();
|
|
101
|
+
|
|
102
|
+
if (positionalParams.length <= minParameterCount) return;
|
|
103
|
+
|
|
104
|
+
// Check for adjacent parameters with the same type
|
|
105
|
+
bool hasAdjacentSameType = _hasAdjacentSameTypeParameters(positionalParams);
|
|
106
|
+
|
|
107
|
+
if (hasAdjacentSameType) {
|
|
108
|
+
violations.add(analyzer.createViolation(
|
|
109
|
+
filePath: filePath,
|
|
110
|
+
line: analyzer.getLine(lineInfo, offset),
|
|
111
|
+
column: analyzer.getColumn(lineInfo, offset),
|
|
112
|
+
message: hasNamedParams
|
|
113
|
+
? 'Function "$name" has ${positionalParams.length} positional parameters with adjacent same-type parameters. Consider using named parameters for all non-required parameters.'
|
|
114
|
+
: 'Function "$name" has ${allParams.length} parameters with adjacent same-type parameters. Consider using named parameters.',
|
|
115
|
+
));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
bool _hasAdjacentSameTypeParameters(List<FormalParameter> params) {
|
|
120
|
+
if (params.length < 2) return false;
|
|
121
|
+
|
|
122
|
+
for (int i = 0; i < params.length - 1; i++) {
|
|
123
|
+
final currentType = _getParameterType(params[i]);
|
|
124
|
+
final nextType = _getParameterType(params[i + 1]);
|
|
125
|
+
|
|
126
|
+
// If both types are not null and they match, we have adjacent same-type parameters
|
|
127
|
+
if (currentType != null && nextType != null && currentType == nextType) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
String? _getParameterType(FormalParameter param) {
|
|
136
|
+
// Handle different parameter types
|
|
137
|
+
if (param is SimpleFormalParameter) {
|
|
138
|
+
return param.type?.toSource();
|
|
139
|
+
} else if (param is DefaultFormalParameter) {
|
|
140
|
+
final innerParam = param.parameter;
|
|
141
|
+
if (innerParam is SimpleFormalParameter) {
|
|
142
|
+
return innerParam.type?.toSource();
|
|
143
|
+
} else if (innerParam is FieldFormalParameter) {
|
|
144
|
+
return innerParam.type?.toSource();
|
|
145
|
+
}
|
|
146
|
+
} else if (param is FieldFormalParameter) {
|
|
147
|
+
return param.type?.toSource();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
/// D012: Prefer Named Boolean Parameters
|
|
10
|
+
/// Boolean parameters should be named or use separate functions for better readability
|
|
11
|
+
class D012PreferNamedBooleanParametersAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'D012';
|
|
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 = _D012Visitor(
|
|
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 _D012Visitor extends RecursiveAstVisitor<void> {
|
|
38
|
+
final String filePath;
|
|
39
|
+
final LineInfo lineInfo;
|
|
40
|
+
final List<Violation> violations;
|
|
41
|
+
final D012PreferNamedBooleanParametersAnalyzer analyzer;
|
|
42
|
+
|
|
43
|
+
_D012Visitor({
|
|
44
|
+
required this.filePath,
|
|
45
|
+
required this.lineInfo,
|
|
46
|
+
required this.violations,
|
|
47
|
+
required this.analyzer,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
@override
|
|
51
|
+
void visitFunctionDeclaration(FunctionDeclaration node) {
|
|
52
|
+
_checkBooleanParameters(
|
|
53
|
+
node.functionExpression.parameters,
|
|
54
|
+
node.name.lexeme,
|
|
55
|
+
node.name.offset,
|
|
56
|
+
);
|
|
57
|
+
super.visitFunctionDeclaration(node);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@override
|
|
61
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
62
|
+
_checkBooleanParameters(
|
|
63
|
+
node.parameters,
|
|
64
|
+
node.name.lexeme,
|
|
65
|
+
node.name.offset,
|
|
66
|
+
);
|
|
67
|
+
super.visitMethodDeclaration(node);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@override
|
|
71
|
+
void visitConstructorDeclaration(ConstructorDeclaration node) {
|
|
72
|
+
final name = node.name?.lexeme ?? '(unnamed constructor)';
|
|
73
|
+
_checkBooleanParameters(
|
|
74
|
+
node.parameters,
|
|
75
|
+
name,
|
|
76
|
+
node.offset,
|
|
77
|
+
);
|
|
78
|
+
super.visitConstructorDeclaration(node);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
void _checkBooleanParameters(FormalParameterList? parameters, String name, int offset) {
|
|
82
|
+
if (parameters == null) return;
|
|
83
|
+
|
|
84
|
+
final allParams = parameters.parameters;
|
|
85
|
+
if (allParams.isEmpty) return;
|
|
86
|
+
|
|
87
|
+
// Get positional boolean parameters
|
|
88
|
+
final positionalBools = <FormalParameter>[];
|
|
89
|
+
final positionalParams = <FormalParameter>[];
|
|
90
|
+
|
|
91
|
+
for (final param in allParams) {
|
|
92
|
+
if (!param.isNamed) {
|
|
93
|
+
positionalParams.add(param);
|
|
94
|
+
if (_isBooleanParameter(param)) {
|
|
95
|
+
positionalBools.add(param);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (positionalBools.isEmpty) return;
|
|
101
|
+
|
|
102
|
+
// Case 1: Single boolean parameter (or only boolean parameters)
|
|
103
|
+
if (positionalParams.length <= 2 && positionalBools.length == 1) {
|
|
104
|
+
violations.add(analyzer.createViolation(
|
|
105
|
+
filePath: filePath,
|
|
106
|
+
line: analyzer.getLine(lineInfo, offset),
|
|
107
|
+
column: analyzer.getColumn(lineInfo, offset),
|
|
108
|
+
message: 'Function "$name" has a boolean parameter. Consider creating separate functions (e.g., ${_generateSeparateFunctionSuggestion(name)}) instead of using a boolean flag.',
|
|
109
|
+
));
|
|
110
|
+
}
|
|
111
|
+
// Case 2: Multiple parameters with boolean(s)
|
|
112
|
+
else if (positionalBools.isNotEmpty) {
|
|
113
|
+
violations.add(analyzer.createViolation(
|
|
114
|
+
filePath: filePath,
|
|
115
|
+
line: analyzer.getLine(lineInfo, offset),
|
|
116
|
+
column: analyzer.getColumn(lineInfo, offset),
|
|
117
|
+
message: 'Function "$name" has boolean parameter(s). Consider using named parameters for boolean values to improve readability at call sites.',
|
|
118
|
+
));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
bool _isBooleanParameter(FormalParameter param) {
|
|
123
|
+
String? typeName;
|
|
124
|
+
|
|
125
|
+
if (param is SimpleFormalParameter) {
|
|
126
|
+
typeName = param.type?.toSource();
|
|
127
|
+
} else if (param is DefaultFormalParameter) {
|
|
128
|
+
final innerParam = param.parameter;
|
|
129
|
+
if (innerParam is SimpleFormalParameter) {
|
|
130
|
+
typeName = innerParam.type?.toSource();
|
|
131
|
+
} else if (innerParam is FieldFormalParameter) {
|
|
132
|
+
typeName = innerParam.type?.toSource();
|
|
133
|
+
}
|
|
134
|
+
} else if (param is FieldFormalParameter) {
|
|
135
|
+
typeName = param.type?.toSource();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return typeName == 'bool' || typeName == 'bool?';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
String _generateSeparateFunctionSuggestion(String functionName) {
|
|
142
|
+
// Generate suggestion for separate function names
|
|
143
|
+
// e.g., "setUser" -> "enableUser/disableUser" or "setUserEnabled/setUserDisabled"
|
|
144
|
+
if (functionName.startsWith('set')) {
|
|
145
|
+
final base = functionName.substring(3);
|
|
146
|
+
return 'enable$base/disable$base';
|
|
147
|
+
} else if (functionName.startsWith('get')) {
|
|
148
|
+
final base = functionName.substring(3);
|
|
149
|
+
return 'get${base}Enabled/get${base}Disabled';
|
|
150
|
+
} else if (functionName.startsWith('is')) {
|
|
151
|
+
return functionName; // is methods are fine
|
|
152
|
+
} else {
|
|
153
|
+
return '${functionName}Enabled/${functionName}Disabled';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|