@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,214 @@
|
|
|
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
|
+
/// C012: Command Query Separation
|
|
10
|
+
/// Separate commands (modify state) from queries (return data)
|
|
11
|
+
/// - Commands should return void
|
|
12
|
+
/// - Queries should be pure (no side effects)
|
|
13
|
+
class C012CommandQuerySeparationAnalyzer extends BaseAnalyzer {
|
|
14
|
+
@override
|
|
15
|
+
String get ruleId => 'C012';
|
|
16
|
+
|
|
17
|
+
/// Patterns that indicate state modification (command)
|
|
18
|
+
static final Set<String> commandIndicators = {
|
|
19
|
+
'set', 'update', 'delete', 'remove', 'add', 'insert',
|
|
20
|
+
'save', 'store', 'persist', 'write', 'modify', 'mutate',
|
|
21
|
+
'push', 'pop', 'shift', 'create', 'destroy', 'clear',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/// Patterns that indicate data retrieval (query)
|
|
25
|
+
static final Set<String> queryIndicators = {
|
|
26
|
+
'get', 'find', 'fetch', 'retrieve', 'load', 'read',
|
|
27
|
+
'calculate', 'compute', 'is', 'has', 'can', 'should',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
@override
|
|
31
|
+
List<Violation> analyze({
|
|
32
|
+
required CompilationUnit unit,
|
|
33
|
+
required String filePath,
|
|
34
|
+
required Rule rule,
|
|
35
|
+
required LineInfo lineInfo,
|
|
36
|
+
}) {
|
|
37
|
+
final violations = <Violation>[];
|
|
38
|
+
|
|
39
|
+
final visitor = _CQSVisitor(
|
|
40
|
+
filePath: filePath,
|
|
41
|
+
lineInfo: lineInfo,
|
|
42
|
+
violations: violations,
|
|
43
|
+
analyzer: this,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
unit.accept(visitor);
|
|
47
|
+
|
|
48
|
+
return violations;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class _CQSVisitor extends RecursiveAstVisitor<void> {
|
|
53
|
+
final String filePath;
|
|
54
|
+
final LineInfo lineInfo;
|
|
55
|
+
final List<Violation> violations;
|
|
56
|
+
final C012CommandQuerySeparationAnalyzer analyzer;
|
|
57
|
+
|
|
58
|
+
_CQSVisitor({
|
|
59
|
+
required this.filePath,
|
|
60
|
+
required this.lineInfo,
|
|
61
|
+
required this.violations,
|
|
62
|
+
required this.analyzer,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
@override
|
|
66
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
67
|
+
// Skip getters, setters, and operators
|
|
68
|
+
if (node.isGetter || node.isSetter || node.isOperator) {
|
|
69
|
+
super.visitMethodDeclaration(node);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_checkCQS(node.name.lexeme, node.returnType, node.body, node.offset);
|
|
74
|
+
super.visitMethodDeclaration(node);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@override
|
|
78
|
+
void visitFunctionDeclaration(FunctionDeclaration node) {
|
|
79
|
+
_checkCQS(
|
|
80
|
+
node.name.lexeme,
|
|
81
|
+
node.returnType,
|
|
82
|
+
node.functionExpression.body,
|
|
83
|
+
node.offset,
|
|
84
|
+
);
|
|
85
|
+
super.visitFunctionDeclaration(node);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
void _checkCQS(
|
|
89
|
+
String name,
|
|
90
|
+
TypeAnnotation? returnType,
|
|
91
|
+
FunctionBody? body,
|
|
92
|
+
int offset,
|
|
93
|
+
) {
|
|
94
|
+
final lowerName = name.toLowerCase();
|
|
95
|
+
final returnTypeStr = returnType?.toSource() ?? '';
|
|
96
|
+
|
|
97
|
+
// Check if it's a command (modifies state)
|
|
98
|
+
final isCommand = _isCommand(lowerName, body);
|
|
99
|
+
|
|
100
|
+
// Check if it returns a value (not void/Future<void>)
|
|
101
|
+
final returnsValue = _returnsValue(returnTypeStr, body);
|
|
102
|
+
|
|
103
|
+
// CQS violation: command that returns value
|
|
104
|
+
if (isCommand && returnsValue) {
|
|
105
|
+
violations.add(analyzer.createViolation(
|
|
106
|
+
filePath: filePath,
|
|
107
|
+
line: analyzer.getLine(lineInfo, offset),
|
|
108
|
+
column: analyzer.getColumn(lineInfo, offset),
|
|
109
|
+
message:
|
|
110
|
+
'Method "$name" violates CQS: modifies state AND returns value. Separate into command and query.',
|
|
111
|
+
metadata: {
|
|
112
|
+
'method': name,
|
|
113
|
+
'returnType': returnTypeStr,
|
|
114
|
+
'issue': 'cqs_violation',
|
|
115
|
+
'suggestion': 'Split into void command method and separate query method',
|
|
116
|
+
},
|
|
117
|
+
));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
bool _isCommand(String name, FunctionBody? body) {
|
|
122
|
+
// Check name for command indicators
|
|
123
|
+
for (final indicator in C012CommandQuerySeparationAnalyzer.commandIndicators) {
|
|
124
|
+
if (name.startsWith(indicator) || name.contains(indicator.capitalize())) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check body for state modifications
|
|
130
|
+
if (body is BlockFunctionBody) {
|
|
131
|
+
final finder = _StateMutationFinder();
|
|
132
|
+
body.block.accept(finder);
|
|
133
|
+
return finder.hasMutation;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
bool _returnsValue(String returnType, FunctionBody? body) {
|
|
140
|
+
// void or Future<void> don't return values
|
|
141
|
+
if (returnType.isEmpty ||
|
|
142
|
+
returnType == 'void' ||
|
|
143
|
+
returnType == 'Future<void>') {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check if body has non-void return statements
|
|
148
|
+
if (body is BlockFunctionBody) {
|
|
149
|
+
final finder = _ReturnFinder();
|
|
150
|
+
body.block.accept(finder);
|
|
151
|
+
return finder.hasNonVoidReturn;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Expression body always returns something
|
|
155
|
+
if (body is ExpressionFunctionBody) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return returnType.isNotEmpty && returnType != 'void';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
class _StateMutationFinder extends RecursiveAstVisitor<void> {
|
|
164
|
+
bool hasMutation = false;
|
|
165
|
+
|
|
166
|
+
@override
|
|
167
|
+
void visitAssignmentExpression(AssignmentExpression node) {
|
|
168
|
+
// Check if it's a field assignment (this.x = ..., _x = ...)
|
|
169
|
+
final target = node.leftHandSide;
|
|
170
|
+
if (target is PrefixedIdentifier || target is PropertyAccess) {
|
|
171
|
+
hasMutation = true;
|
|
172
|
+
} else if (target is SimpleIdentifier) {
|
|
173
|
+
final name = target.name;
|
|
174
|
+
if (name.startsWith('_') || name.startsWith('this.')) {
|
|
175
|
+
hasMutation = true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
super.visitAssignmentExpression(node);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@override
|
|
182
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
183
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
184
|
+
// Check for mutation method calls
|
|
185
|
+
final mutationMethods = {
|
|
186
|
+
'add', 'remove', 'clear', 'insert', 'update', 'delete',
|
|
187
|
+
'push', 'pop', 'shift', 'unshift', 'splice',
|
|
188
|
+
'save', 'store', 'persist', 'write',
|
|
189
|
+
};
|
|
190
|
+
if (mutationMethods.contains(methodName)) {
|
|
191
|
+
hasMutation = true;
|
|
192
|
+
}
|
|
193
|
+
super.visitMethodInvocation(node);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
class _ReturnFinder extends RecursiveAstVisitor<void> {
|
|
198
|
+
bool hasNonVoidReturn = false;
|
|
199
|
+
|
|
200
|
+
@override
|
|
201
|
+
void visitReturnStatement(ReturnStatement node) {
|
|
202
|
+
if (node.expression != null) {
|
|
203
|
+
hasNonVoidReturn = true;
|
|
204
|
+
}
|
|
205
|
+
super.visitReturnStatement(node);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
extension StringExtension on String {
|
|
210
|
+
String capitalize() {
|
|
211
|
+
if (isEmpty) return this;
|
|
212
|
+
return '${this[0].toUpperCase()}${substring(1)}';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import 'package:analyzer/dart/ast/ast.dart';
|
|
2
|
+
import 'package:analyzer/dart/ast/visitor.dart';
|
|
3
|
+
import 'package:analyzer/dart/element/element.dart';
|
|
4
|
+
import 'package:analyzer/source/line_info.dart';
|
|
5
|
+
|
|
6
|
+
import '../../models/rule.dart';
|
|
7
|
+
import '../../models/violation.dart';
|
|
8
|
+
import '../base_analyzer.dart';
|
|
9
|
+
|
|
10
|
+
/// C013: No Dead Code
|
|
11
|
+
/// Detect and eliminate dead code including unreachable statements
|
|
12
|
+
class C013NoDeadCodeAnalyzer extends BaseAnalyzer {
|
|
13
|
+
@override
|
|
14
|
+
String get ruleId => 'C013';
|
|
15
|
+
|
|
16
|
+
@override
|
|
17
|
+
List<Violation> analyze({
|
|
18
|
+
required CompilationUnit unit,
|
|
19
|
+
required String filePath,
|
|
20
|
+
required Rule rule,
|
|
21
|
+
required LineInfo lineInfo,
|
|
22
|
+
}) {
|
|
23
|
+
final violations = <Violation>[];
|
|
24
|
+
|
|
25
|
+
final visitor = _DeadCodeVisitor(
|
|
26
|
+
filePath: filePath,
|
|
27
|
+
lineInfo: lineInfo,
|
|
28
|
+
violations: violations,
|
|
29
|
+
analyzer: this,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
unit.accept(visitor);
|
|
33
|
+
|
|
34
|
+
return violations;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class _DeadCodeVisitor extends RecursiveAstVisitor<void> {
|
|
39
|
+
final String filePath;
|
|
40
|
+
final LineInfo lineInfo;
|
|
41
|
+
final List<Violation> violations;
|
|
42
|
+
final C013NoDeadCodeAnalyzer analyzer;
|
|
43
|
+
|
|
44
|
+
// Track const/final boolean variables and their values
|
|
45
|
+
final Map<String, bool> _constantBooleans = {};
|
|
46
|
+
|
|
47
|
+
_DeadCodeVisitor({
|
|
48
|
+
required this.filePath,
|
|
49
|
+
required this.lineInfo,
|
|
50
|
+
required this.violations,
|
|
51
|
+
required this.analyzer,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
@override
|
|
55
|
+
void visitVariableDeclarationStatement(VariableDeclarationStatement node) {
|
|
56
|
+
// Track const/final boolean variables
|
|
57
|
+
if (node.variables.isConst || node.variables.isFinal) {
|
|
58
|
+
for (final variable in node.variables.variables) {
|
|
59
|
+
final initializer = variable.initializer;
|
|
60
|
+
if (initializer is BooleanLiteral) {
|
|
61
|
+
_constantBooleans[variable.name.lexeme] = initializer.value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
super.visitVariableDeclarationStatement(node);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@override
|
|
69
|
+
void visitBlock(Block node) {
|
|
70
|
+
bool foundTerminator = false;
|
|
71
|
+
Statement? terminatorStatement;
|
|
72
|
+
|
|
73
|
+
for (final statement in node.statements) {
|
|
74
|
+
if (foundTerminator) {
|
|
75
|
+
violations.add(analyzer.createViolation(
|
|
76
|
+
filePath: filePath,
|
|
77
|
+
line: analyzer.getLine(lineInfo, statement.offset),
|
|
78
|
+
column: analyzer.getColumn(lineInfo, statement.offset),
|
|
79
|
+
message:
|
|
80
|
+
'Unreachable code after ${_getTerminatorName(terminatorStatement!)}',
|
|
81
|
+
metadata: {
|
|
82
|
+
'issue': 'unreachable_code',
|
|
83
|
+
'terminator': _getTerminatorName(terminatorStatement),
|
|
84
|
+
},
|
|
85
|
+
));
|
|
86
|
+
break; // Only report once per block
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (_isTerminator(statement)) {
|
|
90
|
+
foundTerminator = true;
|
|
91
|
+
terminatorStatement = statement;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
super.visitBlock(node);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@override
|
|
99
|
+
void visitIfStatement(IfStatement node) {
|
|
100
|
+
final condition = node.expression;
|
|
101
|
+
|
|
102
|
+
// Check for constant true condition
|
|
103
|
+
if (_isConstantTrue(condition)) {
|
|
104
|
+
if (node.elseStatement != null) {
|
|
105
|
+
violations.add(analyzer.createViolation(
|
|
106
|
+
filePath: filePath,
|
|
107
|
+
line: analyzer.getLine(lineInfo, node.elseStatement!.offset),
|
|
108
|
+
column: analyzer.getColumn(lineInfo, node.elseStatement!.offset),
|
|
109
|
+
message: 'Unreachable code block',
|
|
110
|
+
metadata: {
|
|
111
|
+
'issue': 'unreachable_code',
|
|
112
|
+
'reason': 'condition is always true',
|
|
113
|
+
},
|
|
114
|
+
));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check for constant false condition
|
|
119
|
+
if (_isConstantFalse(condition)) {
|
|
120
|
+
violations.add(analyzer.createViolation(
|
|
121
|
+
filePath: filePath,
|
|
122
|
+
line: analyzer.getLine(lineInfo, node.thenStatement.offset),
|
|
123
|
+
column: analyzer.getColumn(lineInfo, node.thenStatement.offset),
|
|
124
|
+
message: 'Unreachable code block',
|
|
125
|
+
metadata: {
|
|
126
|
+
'issue': 'unreachable_code',
|
|
127
|
+
'reason': 'condition is always false',
|
|
128
|
+
},
|
|
129
|
+
));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
super.visitIfStatement(node);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
bool _isConstantTrue(Expression expr) {
|
|
136
|
+
if (expr is BooleanLiteral) return expr.value;
|
|
137
|
+
if (expr is SimpleIdentifier) {
|
|
138
|
+
// Check our tracked constant booleans
|
|
139
|
+
if (_constantBooleans.containsKey(expr.name)) {
|
|
140
|
+
return _constantBooleans[expr.name] == true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
final element = expr.staticElement;
|
|
144
|
+
|
|
145
|
+
// Check for variable elements (local, top-level, fields)
|
|
146
|
+
if (element is VariableElement) {
|
|
147
|
+
if (element.isConst || element.isFinal) {
|
|
148
|
+
final value = element.computeConstantValue();
|
|
149
|
+
if (value != null) {
|
|
150
|
+
return value.toBoolValue() == true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check for property accessors
|
|
156
|
+
if (element is PropertyAccessorElement) {
|
|
157
|
+
final variable = element.variable;
|
|
158
|
+
if (variable.isConst || variable.isFinal) {
|
|
159
|
+
final value = variable.computeConstantValue();
|
|
160
|
+
if (value != null) {
|
|
161
|
+
return value.toBoolValue() == true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
bool _isConstantFalse(Expression expr) {
|
|
170
|
+
if (expr is BooleanLiteral) return !expr.value;
|
|
171
|
+
if (expr is SimpleIdentifier) {
|
|
172
|
+
// Check our tracked constant booleans
|
|
173
|
+
if (_constantBooleans.containsKey(expr.name)) {
|
|
174
|
+
return _constantBooleans[expr.name] == false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
final element = expr.staticElement;
|
|
178
|
+
|
|
179
|
+
// Check for variable elements (local, top-level, fields)
|
|
180
|
+
if (element is VariableElement) {
|
|
181
|
+
if (element.isConst || element.isFinal) {
|
|
182
|
+
final value = element.computeConstantValue();
|
|
183
|
+
if (value != null) {
|
|
184
|
+
return value.toBoolValue() == false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check for property accessors
|
|
190
|
+
if (element is PropertyAccessorElement) {
|
|
191
|
+
final variable = element.variable;
|
|
192
|
+
if (variable.isConst || variable.isFinal) {
|
|
193
|
+
final value = variable.computeConstantValue();
|
|
194
|
+
if (value != null) {
|
|
195
|
+
return value.toBoolValue() == false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
bool _isTerminator(Statement statement) {
|
|
204
|
+
if (statement is ReturnStatement) return true;
|
|
205
|
+
if (statement is ThrowExpression) return true;
|
|
206
|
+
if (statement is BreakStatement) return true;
|
|
207
|
+
if (statement is ContinueStatement) return true;
|
|
208
|
+
if (statement is ExpressionStatement) {
|
|
209
|
+
final expr = statement.expression;
|
|
210
|
+
if (expr is ThrowExpression) return true;
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
String _getTerminatorName(Statement statement) {
|
|
216
|
+
if (statement is ReturnStatement) return 'return';
|
|
217
|
+
if (statement is ThrowExpression) return 'throw';
|
|
218
|
+
if (statement is BreakStatement) return 'break';
|
|
219
|
+
if (statement is ContinueStatement) return 'continue';
|
|
220
|
+
if (statement is ExpressionStatement) {
|
|
221
|
+
if (statement.expression is ThrowExpression) return 'throw';
|
|
222
|
+
}
|
|
223
|
+
return 'terminator';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
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
|
+
/// C014: Dependency Injection
|
|
10
|
+
/// Use dependency injection instead of hardcoded dependencies
|
|
11
|
+
class C014DependencyInjectionAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'C014';
|
|
14
|
+
|
|
15
|
+
/// Patterns that indicate infrastructure dependencies
|
|
16
|
+
static final Set<String> infraPatterns = {
|
|
17
|
+
'Client',
|
|
18
|
+
'Repository',
|
|
19
|
+
'Service',
|
|
20
|
+
'Gateway',
|
|
21
|
+
'Adapter',
|
|
22
|
+
'Provider',
|
|
23
|
+
'Factory',
|
|
24
|
+
'Manager',
|
|
25
|
+
'Handler',
|
|
26
|
+
'Controller',
|
|
27
|
+
'Processor',
|
|
28
|
+
'Validator',
|
|
29
|
+
'Logger',
|
|
30
|
+
'Formatter',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/// Allowed built-in types
|
|
34
|
+
static final Set<String> allowedBuiltins = {
|
|
35
|
+
'DateTime',
|
|
36
|
+
'Duration',
|
|
37
|
+
'List',
|
|
38
|
+
'Map',
|
|
39
|
+
'Set',
|
|
40
|
+
'String',
|
|
41
|
+
'int',
|
|
42
|
+
'double',
|
|
43
|
+
'bool',
|
|
44
|
+
'Object',
|
|
45
|
+
'dynamic',
|
|
46
|
+
'Future',
|
|
47
|
+
'Stream',
|
|
48
|
+
'StreamController',
|
|
49
|
+
'Completer',
|
|
50
|
+
'Timer',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
@override
|
|
54
|
+
List<Violation> analyze({
|
|
55
|
+
required CompilationUnit unit,
|
|
56
|
+
required String filePath,
|
|
57
|
+
required Rule rule,
|
|
58
|
+
required LineInfo lineInfo,
|
|
59
|
+
}) {
|
|
60
|
+
final violations = <Violation>[];
|
|
61
|
+
|
|
62
|
+
final visitor = _DependencyInjectionVisitor(
|
|
63
|
+
filePath: filePath,
|
|
64
|
+
lineInfo: lineInfo,
|
|
65
|
+
violations: violations,
|
|
66
|
+
analyzer: this,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
unit.accept(visitor);
|
|
70
|
+
|
|
71
|
+
return violations;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class _DependencyInjectionVisitor extends RecursiveAstVisitor<void> {
|
|
76
|
+
final String filePath;
|
|
77
|
+
final LineInfo lineInfo;
|
|
78
|
+
final List<Violation> violations;
|
|
79
|
+
final C014DependencyInjectionAnalyzer analyzer;
|
|
80
|
+
String? currentClassName;
|
|
81
|
+
|
|
82
|
+
_DependencyInjectionVisitor({
|
|
83
|
+
required this.filePath,
|
|
84
|
+
required this.lineInfo,
|
|
85
|
+
required this.violations,
|
|
86
|
+
required this.analyzer,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
@override
|
|
90
|
+
void visitClassDeclaration(ClassDeclaration node) {
|
|
91
|
+
currentClassName = node.name.lexeme;
|
|
92
|
+
super.visitClassDeclaration(node);
|
|
93
|
+
currentClassName = null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@override
|
|
97
|
+
void visitFieldDeclaration(FieldDeclaration node) {
|
|
98
|
+
// Check field initializers
|
|
99
|
+
for (final variable in node.fields.variables) {
|
|
100
|
+
if (variable.initializer != null) {
|
|
101
|
+
variable.initializer!.accept(_InstantiationFinder(
|
|
102
|
+
filePath: filePath,
|
|
103
|
+
lineInfo: lineInfo,
|
|
104
|
+
violations: violations,
|
|
105
|
+
analyzer: analyzer,
|
|
106
|
+
context: 'field initializer',
|
|
107
|
+
));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
super.visitFieldDeclaration(node);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@override
|
|
114
|
+
void visitConstructorDeclaration(ConstructorDeclaration node) {
|
|
115
|
+
// Check initializer list
|
|
116
|
+
for (final initializer in node.initializers) {
|
|
117
|
+
initializer.accept(_InstantiationFinder(
|
|
118
|
+
filePath: filePath,
|
|
119
|
+
lineInfo: lineInfo,
|
|
120
|
+
violations: violations,
|
|
121
|
+
analyzer: analyzer,
|
|
122
|
+
context: 'constructor initializer',
|
|
123
|
+
));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check for direct instantiation in constructor body
|
|
127
|
+
if (node.body is BlockFunctionBody) {
|
|
128
|
+
final body = node.body as BlockFunctionBody;
|
|
129
|
+
body.block.accept(_InstantiationFinder(
|
|
130
|
+
filePath: filePath,
|
|
131
|
+
lineInfo: lineInfo,
|
|
132
|
+
violations: violations,
|
|
133
|
+
analyzer: analyzer,
|
|
134
|
+
context: 'constructor',
|
|
135
|
+
));
|
|
136
|
+
}
|
|
137
|
+
super.visitConstructorDeclaration(node);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@override
|
|
141
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
142
|
+
// Skip if method is a factory or builder
|
|
143
|
+
final name = node.name.lexeme.toLowerCase();
|
|
144
|
+
if (name.startsWith('create') ||
|
|
145
|
+
name.startsWith('build') ||
|
|
146
|
+
name.startsWith('make') ||
|
|
147
|
+
name.contains('factory')) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (node.body is BlockFunctionBody) {
|
|
152
|
+
final body = node.body as BlockFunctionBody;
|
|
153
|
+
body.block.accept(_InstantiationFinder(
|
|
154
|
+
filePath: filePath,
|
|
155
|
+
lineInfo: lineInfo,
|
|
156
|
+
violations: violations,
|
|
157
|
+
analyzer: analyzer,
|
|
158
|
+
context: 'method',
|
|
159
|
+
));
|
|
160
|
+
}
|
|
161
|
+
super.visitMethodDeclaration(node);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
class _InstantiationFinder extends RecursiveAstVisitor<void> {
|
|
166
|
+
final String filePath;
|
|
167
|
+
final LineInfo lineInfo;
|
|
168
|
+
final List<Violation> violations;
|
|
169
|
+
final C014DependencyInjectionAnalyzer analyzer;
|
|
170
|
+
final String context;
|
|
171
|
+
|
|
172
|
+
_InstantiationFinder({
|
|
173
|
+
required this.filePath,
|
|
174
|
+
required this.lineInfo,
|
|
175
|
+
required this.violations,
|
|
176
|
+
required this.analyzer,
|
|
177
|
+
required this.context,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
@override
|
|
181
|
+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
182
|
+
final typeName = node.constructorName.type.name2.lexeme;
|
|
183
|
+
_checkInfraType(typeName, node.offset);
|
|
184
|
+
super.visitInstanceCreationExpression(node);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@override
|
|
188
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
189
|
+
// In Dart, constructor calls without 'new' keyword are parsed as MethodInvocation
|
|
190
|
+
// e.g., UserRepository() is a MethodInvocation, not InstanceCreationExpression
|
|
191
|
+
if (node.target == null) {
|
|
192
|
+
final methodName = node.methodName.name;
|
|
193
|
+
|
|
194
|
+
// Check if this looks like a constructor call:
|
|
195
|
+
// - Starts with uppercase: UserRepository()
|
|
196
|
+
// - Starts with underscore + uppercase: _PrivateClass()
|
|
197
|
+
// NOT:
|
|
198
|
+
// - Starts with lowercase: getData()
|
|
199
|
+
// - Starts with underscore + lowercase: _updateProvider()
|
|
200
|
+
if (methodName.isNotEmpty) {
|
|
201
|
+
final firstChar = methodName[0];
|
|
202
|
+
final isConstructorPattern =
|
|
203
|
+
firstChar.toUpperCase() == firstChar && firstChar != '_' ||
|
|
204
|
+
(firstChar == '_' &&
|
|
205
|
+
methodName.length > 1 &&
|
|
206
|
+
methodName[1].toUpperCase() == methodName[1]);
|
|
207
|
+
|
|
208
|
+
if (isConstructorPattern) {
|
|
209
|
+
_checkInfraType(methodName, node.offset);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
super.visitMethodInvocation(node);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
void _checkInfraType(String typeName, int offset) {
|
|
217
|
+
// Check if it's an infrastructure type
|
|
218
|
+
if (_isInfraType(typeName)) {
|
|
219
|
+
violations.add(analyzer.createViolation(
|
|
220
|
+
filePath: filePath,
|
|
221
|
+
line: analyzer.getLine(lineInfo, offset),
|
|
222
|
+
column: analyzer.getColumn(lineInfo, offset),
|
|
223
|
+
message:
|
|
224
|
+
'Direct instantiation of "$typeName" in $context - consider dependency injection',
|
|
225
|
+
metadata: {
|
|
226
|
+
'type': typeName,
|
|
227
|
+
'context': context,
|
|
228
|
+
'issue': 'direct_instantiation',
|
|
229
|
+
},
|
|
230
|
+
));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
bool _isInfraType(String typeName) {
|
|
235
|
+
// Skip built-in types
|
|
236
|
+
if (C014DependencyInjectionAnalyzer.allowedBuiltins.contains(typeName)) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Check if type name ends with infrastructure patterns
|
|
241
|
+
for (final pattern in C014DependencyInjectionAnalyzer.infraPatterns) {
|
|
242
|
+
if (typeName.endsWith(pattern)) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|