@sun-asterisk/sunlint 1.3.43 → 1.3.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/dart_analyzer/README.md +226 -0
  2. package/dart_analyzer/analysis_options.yaml +66 -0
  3. package/dart_analyzer/bin/sunlint-dart-macos +0 -0
  4. package/dart_analyzer/bin/sunlint_dart_analyzer.dart +124 -0
  5. package/dart_analyzer/lib/analyzer_service.dart +625 -0
  6. package/dart_analyzer/lib/json_rpc_server.dart +275 -0
  7. package/dart_analyzer/lib/models/rule.dart +67 -0
  8. package/dart_analyzer/lib/models/symbol_table.dart +607 -0
  9. package/dart_analyzer/lib/models/violation.dart +69 -0
  10. package/dart_analyzer/lib/rules/base_analyzer.dart +52 -0
  11. package/dart_analyzer/lib/rules/common/C002_no_duplicate_code.dart +344 -0
  12. package/dart_analyzer/lib/rules/common/C003_no_vague_abbreviations.dart +318 -0
  13. package/dart_analyzer/lib/rules/common/C006_function_naming.dart +219 -0
  14. package/dart_analyzer/lib/rules/common/C008_variable_declaration_locality.dart +205 -0
  15. package/dart_analyzer/lib/rules/common/C010_limit_block_nesting.dart +162 -0
  16. package/dart_analyzer/lib/rules/common/C012_command_query_separation.dart +214 -0
  17. package/dart_analyzer/lib/rules/common/C013_no_dead_code.dart +225 -0
  18. package/dart_analyzer/lib/rules/common/C014_dependency_injection.dart +249 -0
  19. package/dart_analyzer/lib/rules/common/C017_constructor_logic.dart +158 -0
  20. package/dart_analyzer/lib/rules/common/C018_no_throw_generic_error.dart +141 -0
  21. package/dart_analyzer/lib/rules/common/C019_log_level_usage.dart +165 -0
  22. package/dart_analyzer/lib/rules/common/C020_unused_imports.dart +128 -0
  23. package/dart_analyzer/lib/rules/common/C021_import_organization.dart +86 -0
  24. package/dart_analyzer/lib/rules/common/C023_no_duplicate_variable.dart +112 -0
  25. package/dart_analyzer/lib/rules/common/C024_no_scatter_hardcoded_constants.dart +79 -0
  26. package/dart_analyzer/lib/rules/common/C029_catch_block_logging.dart +81 -0
  27. package/dart_analyzer/lib/rules/common/C030_use_custom_error_classes.dart +77 -0
  28. package/dart_analyzer/lib/rules/common/C031_validation_separation.dart +90 -0
  29. package/dart_analyzer/lib/rules/common/C033_separate_service_repository.dart +80 -0
  30. package/dart_analyzer/lib/rules/common/C035_error_logging_context.dart +148 -0
  31. package/dart_analyzer/lib/rules/common/C040_centralized_validation.dart +84 -0
  32. package/dart_analyzer/lib/rules/common/C041_no_sensitive_hardcode.dart +103 -0
  33. package/dart_analyzer/lib/rules/common/C042_boolean_name_prefix.dart +105 -0
  34. package/dart_analyzer/lib/rules/common/C043_no_console_or_print.dart +101 -0
  35. package/dart_analyzer/lib/rules/common/C047_no_duplicate_retry_logic.dart +94 -0
  36. package/dart_analyzer/lib/rules/common/C048_no_bypass_architectural_layers.dart +132 -0
  37. package/dart_analyzer/lib/rules/common/C052_parsing_or_data_transformation.dart +95 -0
  38. package/dart_analyzer/lib/rules/common/C060_no_override_superclass.dart +81 -0
  39. package/dart_analyzer/lib/rules/common/C065_one_behavior_per_test.dart +83 -0
  40. package/dart_analyzer/lib/rules/common/C067_no_hardcoded_config.dart +89 -0
  41. package/dart_analyzer/lib/rules/common/C070_no_real_time_tests.dart +99 -0
  42. package/dart_analyzer/lib/rules/common/C072_single_test_behavior.dart +78 -0
  43. package/dart_analyzer/lib/rules/common/C073_validate_required_config_on_startup.dart +82 -0
  44. package/dart_analyzer/lib/rules/common/C075_explicit_return_types.dart +85 -0
  45. package/dart_analyzer/lib/rules/common/C076_explicit_function_types.dart +104 -0
  46. package/dart_analyzer/lib/rules/dart/D001_recommended_lint_rules.dart +309 -0
  47. package/dart_analyzer/lib/rules/dart/D002_dispose_resources.dart +338 -0
  48. package/dart_analyzer/lib/rules/dart/D003_prefer_widgets_over_methods.dart +273 -0
  49. package/dart_analyzer/lib/rules/dart/D004_avoid_shrinkwrap_listview.dart +154 -0
  50. package/dart_analyzer/lib/rules/dart/D005_limit_widget_nesting.dart +265 -0
  51. package/dart_analyzer/lib/rules/dart/D006_prefer_extracting_large_callbacks.dart +135 -0
  52. package/dart_analyzer/lib/rules/dart/D007_prefer_init_first_dispose_last.dart +150 -0
  53. package/dart_analyzer/lib/rules/dart/D008_avoid_long_functions.dart +394 -0
  54. package/dart_analyzer/lib/rules/dart/D009_limit_function_parameters.dart +179 -0
  55. package/dart_analyzer/lib/rules/dart/D010_limit_cyclomatic_complexity.dart +257 -0
  56. package/dart_analyzer/lib/rules/dart/D011_prefer_named_parameters.dart +152 -0
  57. package/dart_analyzer/lib/rules/dart/D012_prefer_named_boolean_parameters.dart +156 -0
  58. package/dart_analyzer/lib/rules/dart/D013_single_public_class.dart +246 -0
  59. package/dart_analyzer/lib/rules/dart/D014_unsafe_collection_access.dart +202 -0
  60. package/dart_analyzer/lib/rules/dart/D015_copywith_all_parameters.dart +125 -0
  61. package/dart_analyzer/lib/rules/dart/D016_project_should_have_tests.dart +134 -0
  62. package/dart_analyzer/lib/rules/dart/D017_pubspec_dependencies_review.dart +187 -0
  63. package/dart_analyzer/lib/rules/dart/D018_remove_commented_code.dart +196 -0
  64. package/dart_analyzer/lib/rules/dart/D019_avoid_single_child_multi_child_widget.dart +161 -0
  65. package/dart_analyzer/lib/rules/dart/D020_limit_if_else_branches.dart +125 -0
  66. package/dart_analyzer/lib/rules/dart/D021_avoid_negated_boolean_checks.dart +227 -0
  67. package/dart_analyzer/lib/rules/dart/D022_use_setstate_correctly.dart +269 -0
  68. package/dart_analyzer/lib/rules/dart/D023_avoid_unnecessary_method_overrides.dart +191 -0
  69. package/dart_analyzer/lib/rules/dart/D024_avoid_unnecessary_stateful_widget.dart +194 -0
  70. package/dart_analyzer/lib/rules/dart/D025_avoid_nested_conditional_expressions.dart +90 -0
  71. package/dart_analyzer/lib/rules/security/S001_backend_auth_communications.dart +155 -0
  72. package/dart_analyzer/lib/rules/security/S002_os_command_injection.dart +159 -0
  73. package/dart_analyzer/lib/rules/security/S003_open_redirect_protection.dart +208 -0
  74. package/dart_analyzer/lib/rules/security/S004_sensitive_data_logging.dart +391 -0
  75. package/dart_analyzer/lib/rules/security/S005_trusted_service_authorization.dart +182 -0
  76. package/dart_analyzer/lib/rules/security/S006_no_default_credentials.dart +208 -0
  77. package/dart_analyzer/lib/rules/security/S007_output_encoding.dart +224 -0
  78. package/dart_analyzer/lib/rules/security/S008_svg_content_sanitization.dart +211 -0
  79. package/dart_analyzer/lib/rules/security/S009_no_insecure_encryption.dart +160 -0
  80. package/dart_analyzer/lib/rules/security/S010_use_csprng.dart +184 -0
  81. package/dart_analyzer/lib/rules/security/S011_ech_tls_config.dart +175 -0
  82. package/dart_analyzer/lib/rules/security/S012_hardcoded_secrets.dart +255 -0
  83. package/dart_analyzer/lib/rules/security/S013_tls_enforcement.dart +148 -0
  84. package/dart_analyzer/lib/rules/security/S014_tls_version_enforcement.dart +117 -0
  85. package/dart_analyzer/lib/rules/security/S015_insecure_tls_certificate.dart +315 -0
  86. package/dart_analyzer/lib/rules/security/S016_no_sensitive_querystring.dart +244 -0
  87. package/dart_analyzer/lib/rules/security/S017_use_parameterized_queries.dart +191 -0
  88. package/dart_analyzer/lib/rules/security/S018_no_sensitive_browser_storage.dart +175 -0
  89. package/dart_analyzer/lib/rules/security/S019_smtp_injection_protection.dart +166 -0
  90. package/dart_analyzer/lib/rules/security/S020_no_eval_dynamic_code.dart +149 -0
  91. package/dart_analyzer/lib/rules/security/S021_referrer_policy.dart +146 -0
  92. package/dart_analyzer/lib/rules/security/S022_escape_output_context.dart +111 -0
  93. package/dart_analyzer/lib/rules/security/S023_no_json_injection.dart +550 -0
  94. package/dart_analyzer/lib/rules/security/S024_xpath_xxe_protection.dart +299 -0
  95. package/dart_analyzer/lib/rules/security/S025_server_side_validation.dart +140 -0
  96. package/dart_analyzer/lib/rules/security/S026_tls_all_connections.dart +196 -0
  97. package/dart_analyzer/lib/rules/security/S027_mtls_certificate_validation.dart +195 -0
  98. package/dart_analyzer/lib/rules/security/S028_file_upload_size_limits.dart +186 -0
  99. package/dart_analyzer/lib/rules/security/S029_csrf_protection.dart +171 -0
  100. package/dart_analyzer/lib/rules/security/S030_directory_browsing_protection.dart +144 -0
  101. package/dart_analyzer/lib/rules/security/S031_secure_session_cookies.dart +118 -0
  102. package/dart_analyzer/lib/rules/security/S032_httponly_session_cookies.dart +114 -0
  103. package/dart_analyzer/lib/rules/security/S033_samesite_session_cookies.dart +120 -0
  104. package/dart_analyzer/lib/rules/security/S034_host_prefix_session_cookies.dart +160 -0
  105. package/dart_analyzer/lib/rules/security/S035_separate_app_hostnames.dart +117 -0
  106. package/dart_analyzer/lib/rules/security/S036_lfi_rfi_protection.dart +188 -0
  107. package/dart_analyzer/lib/rules/security/S037_cache_headers.dart +113 -0
  108. package/dart_analyzer/lib/rules/security/S038_no_version_headers.dart +114 -0
  109. package/dart_analyzer/lib/rules/security/S039_tls_certificate_validation.dart +131 -0
  110. package/dart_analyzer/lib/rules/security/S040_session_fixation_protection.dart +155 -0
  111. package/dart_analyzer/lib/rules/security/S041_session_token_invalidation.dart +201 -0
  112. package/dart_analyzer/lib/rules/security/S042_require_re_authentication_for_long_lived.dart +158 -0
  113. package/dart_analyzer/lib/rules/security/S043_password_changes_invalidate_all_sessions.dart +88 -0
  114. package/dart_analyzer/lib/rules/security/S044_re_authentication_required.dart +119 -0
  115. package/dart_analyzer/lib/rules/security/S045_brute_force_protection.dart +253 -0
  116. package/dart_analyzer/lib/rules/security/S046_jwt_algorithm_allowlist.dart +113 -0
  117. package/dart_analyzer/lib/rules/security/S047_oauth_pkce_protection.dart +124 -0
  118. package/dart_analyzer/lib/rules/security/S048_oauth_redirect_uri_validation.dart +134 -0
  119. package/dart_analyzer/lib/rules/security/S049_short_validity_tokens.dart +145 -0
  120. package/dart_analyzer/lib/rules/security/S050_reference_tokens_entropy.dart +234 -0
  121. package/dart_analyzer/lib/rules/security/S051_password_length_policy.dart +171 -0
  122. package/dart_analyzer/lib/rules/security/S052_weak_otp_entropy.dart +107 -0
  123. package/dart_analyzer/lib/rules/security/S053_generic_error_messages.dart +159 -0
  124. package/dart_analyzer/lib/rules/security/S054_no_default_accounts.dart +141 -0
  125. package/dart_analyzer/lib/rules/security/S055_content_type_validation.dart +324 -0
  126. package/dart_analyzer/lib/rules/security/S056_log_injection_protection.dart +119 -0
  127. package/dart_analyzer/lib/rules/security/S057_utc_logging.dart +114 -0
  128. package/dart_analyzer/lib/rules/security/S058_no_ssrf.dart +175 -0
  129. package/dart_analyzer/lib/rules/security/S059_disable_debug_mode.dart +172 -0
  130. package/dart_analyzer/lib/rules/security/S060_password_minimum_length.dart +170 -0
  131. package/dart_analyzer/lib/symbol_table_extractor.dart +510 -0
  132. package/dart_analyzer/lib/utils/common_utils.dart +26 -0
  133. package/dart_analyzer/pubspec.lock +557 -0
  134. package/dart_analyzer/pubspec.yaml +39 -0
  135. package/dart_analyzer/test/fixtures/complex_code.dart +95 -0
  136. package/docs/GENERATED_FILE_HANDLING_SUMMARY.md +2 -2
  137. package/package.json +3 -2
@@ -0,0 +1,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
+ }