@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,246 @@
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
+ /// D013: Prefer a Single Public Class Per File
10
+ /// Each file should contain only one public class to improve code organization and maintainability
11
+ ///
12
+ /// Exceptions:
13
+ /// - Sealed classes / union types with small implementations (<20 lines each)
14
+ /// - Multiple small classes (<20 lines) that share a common base class
15
+ class D013SinglePublicClassAnalyzer extends BaseAnalyzer {
16
+ @override
17
+ String get ruleId => 'D013';
18
+
19
+ // Maximum lines for a "small" class in a sealed/union pattern
20
+ static const int _maxLinesForSmallClass = 20;
21
+
22
+ @override
23
+ List<Violation> analyze({
24
+ required CompilationUnit unit,
25
+ required String filePath,
26
+ required Rule rule,
27
+ required LineInfo lineInfo,
28
+ }) {
29
+ final violations = <Violation>[];
30
+
31
+ final visitor = _D013Visitor(
32
+ filePath: filePath,
33
+ lineInfo: lineInfo,
34
+ violations: violations,
35
+ analyzer: this,
36
+ );
37
+
38
+ unit.accept(visitor);
39
+
40
+ return violations;
41
+ }
42
+ }
43
+
44
+ class _D013Visitor extends RecursiveAstVisitor<void> {
45
+ final String filePath;
46
+ final LineInfo lineInfo;
47
+ final List<Violation> violations;
48
+ final D013SinglePublicClassAnalyzer analyzer;
49
+
50
+ final List<ClassDeclaration> _publicClasses = [];
51
+ final Map<String, _ClassInfo> _classInfoMap = {};
52
+
53
+ _D013Visitor({
54
+ required this.filePath,
55
+ required this.lineInfo,
56
+ required this.violations,
57
+ required this.analyzer,
58
+ });
59
+
60
+ @override
61
+ void visitCompilationUnit(CompilationUnit node) {
62
+ // First pass: collect all public classes and their metadata
63
+ _publicClasses.clear();
64
+ _classInfoMap.clear();
65
+ super.visitCompilationUnit(node);
66
+
67
+ // Second pass: analyze if multiple public classes are acceptable
68
+ if (_publicClasses.length > 1) {
69
+ // Check for Flutter Widget + public State pattern
70
+ final widgetStateIssue = _checkFlutterWidgetStatePattern();
71
+ if (widgetStateIssue != null) {
72
+ violations.add(widgetStateIssue);
73
+ return;
74
+ }
75
+
76
+ if (!_isAcceptableMultipleClasses()) {
77
+ final firstClass = _publicClasses[0];
78
+ final classNames = _publicClasses.map((c) => c.name.lexeme).join(', ');
79
+
80
+ violations.add(analyzer.createViolation(
81
+ filePath: filePath,
82
+ line: analyzer.getLine(lineInfo, firstClass.name.offset),
83
+ column: analyzer.getColumn(lineInfo, firstClass.name.offset),
84
+ message:
85
+ 'File contains ${_publicClasses.length} public classes: $classNames. Consider moving them to separate files (one public class per file).',
86
+ ));
87
+ }
88
+ }
89
+ }
90
+
91
+ /// Check for Flutter Widget with public State class pattern
92
+ /// Returns a violation if found, null otherwise
93
+ Violation? _checkFlutterWidgetStatePattern() {
94
+ if (_publicClasses.length != 2) return null;
95
+
96
+ ClassDeclaration? widgetClass;
97
+ ClassDeclaration? stateClass;
98
+
99
+ for (final cls in _publicClasses) {
100
+ final info = _classInfoMap[cls.name.lexeme];
101
+ if (info == null) continue;
102
+
103
+ if (info.baseClassName == 'StatefulWidget') {
104
+ widgetClass = cls;
105
+ } else if (info.baseClassName == 'State') {
106
+ stateClass = cls;
107
+ }
108
+ }
109
+
110
+ // If we have a StatefulWidget and a public State class
111
+ if (widgetClass != null && stateClass != null) {
112
+ final widgetName = widgetClass.name.lexeme;
113
+ final stateName = stateClass.name.lexeme;
114
+
115
+ // Check if State class name follows the pattern: WidgetNameState
116
+ // This confirms they're related
117
+ if (stateName.endsWith('State') &&
118
+ (stateName == '${widgetName}State' ||
119
+ stateName.startsWith(widgetName))) {
120
+ return analyzer.createViolation(
121
+ filePath: filePath,
122
+ line: analyzer.getLine(lineInfo, stateClass.name.offset),
123
+ column: analyzer.getColumn(lineInfo, stateClass.name.offset),
124
+ message:
125
+ 'State class "$stateName" should be private (prefix with underscore: _$stateName). Public State classes expose internal implementation details.',
126
+ );
127
+ }
128
+ }
129
+
130
+ return null;
131
+ }
132
+
133
+ /// Check if multiple public classes are acceptable (sealed class pattern)
134
+ bool _isAcceptableMultipleClasses() {
135
+ // Find sealed classes or potential base classes
136
+ final sealedClasses = <String>[];
137
+ final baseClasses = <String>{};
138
+
139
+ for (final cls in _publicClasses) {
140
+ final className = cls.name.lexeme;
141
+ final info = _classInfoMap[className];
142
+ if (info == null) continue;
143
+
144
+ // Check if it's a sealed class (has 'sealed' modifier)
145
+ if (info.isSealed) {
146
+ sealedClasses.add(className);
147
+ }
148
+
149
+ // Check if other classes extend/implement this class
150
+ final childCount = _publicClasses.where((c) {
151
+ final childInfo = _classInfoMap[c.name.lexeme];
152
+ return childInfo != null && childInfo.baseClassName == className;
153
+ }).length;
154
+
155
+ if (childCount > 0) {
156
+ baseClasses.add(className);
157
+ }
158
+ }
159
+
160
+ // If we have sealed/base classes, check if all child classes are small
161
+ if (sealedClasses.isNotEmpty || baseClasses.isNotEmpty) {
162
+ final allBasesAndChildren = <String>{...sealedClasses, ...baseClasses};
163
+
164
+ // Get all classes that extend a base class
165
+ final childClasses = _publicClasses.where((cls) {
166
+ final info = _classInfoMap[cls.name.lexeme];
167
+ return info != null &&
168
+ info.baseClassName != null &&
169
+ allBasesAndChildren.contains(info.baseClassName);
170
+ }).toList();
171
+
172
+ // Check if there are any standalone classes (not base, not child)
173
+ final standaloneClasses = _publicClasses.where((cls) {
174
+ final className = cls.name.lexeme;
175
+ final info = _classInfoMap[className];
176
+ return !allBasesAndChildren.contains(className) &&
177
+ (info?.baseClassName == null ||
178
+ !allBasesAndChildren.contains(info?.baseClassName));
179
+ }).toList();
180
+
181
+ // Allow if:
182
+ // 1. All child classes are small (<20 lines)
183
+ // 2. There's at most one standalone class (the main class of the file)
184
+ final allChildrenSmall = childClasses.every((cls) {
185
+ final info = _classInfoMap[cls.name.lexeme];
186
+ return info != null &&
187
+ info.lineCount <=
188
+ D013SinglePublicClassAnalyzer._maxLinesForSmallClass;
189
+ });
190
+
191
+ return allChildrenSmall && standaloneClasses.length <= 1;
192
+ }
193
+
194
+ return false;
195
+ }
196
+
197
+ @override
198
+ void visitClassDeclaration(ClassDeclaration node) {
199
+ final className = node.name.lexeme;
200
+
201
+ // Check if class is public (doesn't start with underscore)
202
+ if (!className.startsWith('_')) {
203
+ _publicClasses.add(node);
204
+
205
+ // Collect metadata about the class
206
+ final startLine = lineInfo.getLocation(node.offset).lineNumber;
207
+ final endLine = lineInfo.getLocation(node.end).lineNumber;
208
+ final lineCount = endLine - startLine + 1;
209
+
210
+ // Check if it's a sealed class
211
+ final isSealed = node.sealedKeyword != null;
212
+
213
+ // Get base class name if any
214
+ String? baseClassName;
215
+ if (node.extendsClause != null) {
216
+ baseClassName = node.extendsClause!.superclass.name2.lexeme;
217
+ } else if (node.implementsClause != null &&
218
+ node.implementsClause!.interfaces.isNotEmpty) {
219
+ baseClassName = node.implementsClause!.interfaces.first.name2.lexeme;
220
+ }
221
+
222
+ _classInfoMap[className] = _ClassInfo(
223
+ className: className,
224
+ lineCount: lineCount,
225
+ isSealed: isSealed,
226
+ baseClassName: baseClassName,
227
+ );
228
+ }
229
+
230
+ super.visitClassDeclaration(node);
231
+ }
232
+ }
233
+
234
+ class _ClassInfo {
235
+ final String className;
236
+ final int lineCount;
237
+ final bool isSealed;
238
+ final String? baseClassName;
239
+
240
+ _ClassInfo({
241
+ required this.className,
242
+ required this.lineCount,
243
+ required this.isSealed,
244
+ this.baseClassName,
245
+ });
246
+ }
@@ -0,0 +1,202 @@
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
+ /// D014: Avoid Unsafe Collection Access
10
+ /// Always check collection empty or length before using first, last, single, or elementAt
11
+ class D014UnsafeCollectionAccessAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'D014';
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 = _D014Visitor(
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 _D014Visitor extends RecursiveAstVisitor<void> {
38
+ final String filePath;
39
+ final LineInfo lineInfo;
40
+ final List<Violation> violations;
41
+ final D014UnsafeCollectionAccessAnalyzer analyzer;
42
+
43
+ // Unsafe collection access properties
44
+ static const _unsafeProperties = ['first', 'last', 'single'];
45
+
46
+ // Unsafe collection access methods
47
+ static const _unsafeMethods = ['firstWhere', 'lastWhere', 'singleWhere', 'elementAt'];
48
+
49
+ // Safe alternatives
50
+ static const _safeAlternatives = {
51
+ 'first': 'firstOrNull',
52
+ 'last': 'lastOrNull',
53
+ 'single': 'singleOrNull',
54
+ 'firstWhere': 'orElse parameter',
55
+ 'lastWhere': 'orElse parameter',
56
+ 'singleWhere': 'orElse parameter',
57
+ 'elementAt': 'length check',
58
+ };
59
+
60
+ _D014Visitor({
61
+ required this.filePath,
62
+ required this.lineInfo,
63
+ required this.violations,
64
+ required this.analyzer,
65
+ });
66
+
67
+ @override
68
+ void visitPropertyAccess(PropertyAccess node) {
69
+ final propertyName = node.propertyName.name;
70
+
71
+ // Check if it's one of the unsafe properties
72
+ if (_unsafeProperties.contains(propertyName)) {
73
+ // Skip if using null-aware operator (?.)
74
+ if (node.operator.type.toString() == '?.') {
75
+ super.visitPropertyAccess(node);
76
+ return;
77
+ }
78
+
79
+ // Check if it's protected by a safety check
80
+ if (!_isProtectedByCheck(node)) {
81
+ final alternative = _safeAlternatives[propertyName] ?? 'safe alternative';
82
+ violations.add(analyzer.createViolation(
83
+ filePath: filePath,
84
+ line: analyzer.getLine(lineInfo, node.propertyName.offset),
85
+ column: analyzer.getColumn(lineInfo, node.propertyName.offset),
86
+ message:
87
+ 'Unsafe collection access ".$propertyName" may throw if collection is empty. Consider checking isEmpty/length first or use ".$alternative" from collection package.',
88
+ ));
89
+ }
90
+ }
91
+
92
+ super.visitPropertyAccess(node);
93
+ }
94
+
95
+ @override
96
+ void visitMethodInvocation(MethodInvocation node) {
97
+ final methodName = node.methodName.name;
98
+
99
+ // Check for unsafe methods like elementAt, firstWhere, lastWhere, singleWhere
100
+ if (_unsafeMethods.contains(methodName)) {
101
+ // For firstWhere, lastWhere, singleWhere - check if orElse parameter is provided
102
+ if (['firstWhere', 'lastWhere', 'singleWhere'].contains(methodName)) {
103
+ // Check if orElse parameter is provided
104
+ final hasOrElse = node.argumentList.arguments.any((arg) {
105
+ if (arg is NamedExpression) {
106
+ return arg.name.label.name == 'orElse';
107
+ }
108
+ return false;
109
+ });
110
+
111
+ if (hasOrElse) {
112
+ super.visitMethodInvocation(node);
113
+ return;
114
+ }
115
+ }
116
+
117
+ if (!_isProtectedByCheck(node)) {
118
+ String message;
119
+ if (methodName == 'elementAt') {
120
+ message = 'Unsafe collection access ".$methodName()" may throw if index is out of bounds. Consider checking length first.';
121
+ } else {
122
+ message = 'Unsafe collection access ".$methodName()" may throw if element not found. Consider providing an "orElse" parameter.';
123
+ }
124
+
125
+ violations.add(analyzer.createViolation(
126
+ filePath: filePath,
127
+ line: analyzer.getLine(lineInfo, node.methodName.offset),
128
+ column: analyzer.getColumn(lineInfo, node.methodName.offset),
129
+ message: message,
130
+ ));
131
+ }
132
+ }
133
+
134
+ super.visitMethodInvocation(node);
135
+ }
136
+
137
+ @override
138
+ void visitIndexExpression(IndexExpression node) {
139
+ // Check if target is a Map (skip Maps as per spec)
140
+ final targetType = node.target?.staticType?.toString() ?? '';
141
+
142
+ // Skip if it's a Map type
143
+ if (targetType.contains('Map<') || targetType == 'Map') {
144
+ super.visitIndexExpression(node);
145
+ return;
146
+ }
147
+
148
+ // Check if it's on a collection (List, Iterable, etc.)
149
+ if (targetType.contains('List<') ||
150
+ targetType.contains('Iterable<') ||
151
+ targetType == 'List' ||
152
+ targetType == 'Iterable') {
153
+ if (!_isProtectedByCheck(node)) {
154
+ violations.add(analyzer.createViolation(
155
+ filePath: filePath,
156
+ line: analyzer.getLine(lineInfo, node.leftBracket.offset),
157
+ column: analyzer.getColumn(lineInfo, node.leftBracket.offset),
158
+ message:
159
+ 'Unsafe collection access using [] operator may throw if index is out of bounds. Consider checking length first or use "elementAtOrNull" from collection package.',
160
+ ));
161
+ }
162
+ }
163
+
164
+ super.visitIndexExpression(node);
165
+ }
166
+
167
+ /// Check if the collection access is protected by a safety check
168
+ bool _isProtectedByCheck(AstNode node) {
169
+ AstNode? current = node.parent;
170
+
171
+ while (current != null) {
172
+ // Check if inside an if statement with safety check
173
+ if (current is IfStatement) {
174
+ final condition = current.expression;
175
+ if (_containsSafetyCheck(condition)) {
176
+ return true;
177
+ }
178
+ }
179
+
180
+ // Check if inside a conditional expression with safety check
181
+ if (current is ConditionalExpression) {
182
+ if (_containsSafetyCheck(current.condition)) {
183
+ return true;
184
+ }
185
+ }
186
+
187
+ current = current.parent;
188
+ }
189
+
190
+ return false;
191
+ }
192
+
193
+ /// Check if an expression contains safety checks like isEmpty, isNotEmpty, length > 0
194
+ bool _containsSafetyCheck(Expression expr) {
195
+ final exprSource = expr.toString();
196
+
197
+ // Check for common safety patterns
198
+ return exprSource.contains('isEmpty') ||
199
+ exprSource.contains('isNotEmpty') ||
200
+ exprSource.contains('.length');
201
+ }
202
+ }
@@ -0,0 +1,125 @@
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
+ /// D015: Ensure copyWith includes all constructor parameters
10
+ /// When a class has a copyWith method, it should include all constructor parameters
11
+ class D015CopyWithAllParametersAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'D015';
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 = _D015Visitor(
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 _D015Visitor extends RecursiveAstVisitor<void> {
38
+ final String filePath;
39
+ final LineInfo lineInfo;
40
+ final List<Violation> violations;
41
+ final D015CopyWithAllParametersAnalyzer analyzer;
42
+
43
+ // Track classes being analyzed
44
+ ClassDeclaration? _currentClass;
45
+ final Map<String, Set<String>> _classConstructorParams = {};
46
+ final Map<String, Set<String>> _classCopyWithParams = {};
47
+ final Map<String, MethodDeclaration> _classCopyWithMethods = {};
48
+
49
+ _D015Visitor({
50
+ required this.filePath,
51
+ required this.lineInfo,
52
+ required this.violations,
53
+ required this.analyzer,
54
+ });
55
+
56
+ @override
57
+ void visitClassDeclaration(ClassDeclaration node) {
58
+ _currentClass = node;
59
+ final className = node.name.lexeme;
60
+
61
+ // Collect constructor parameters
62
+ final constructorParams = <String>{};
63
+ for (final member in node.members) {
64
+ if (member is ConstructorDeclaration && member.name == null) {
65
+ // Default constructor
66
+ for (final param in member.parameters.parameters) {
67
+ if (param is DefaultFormalParameter) {
68
+ constructorParams.add(param.parameter.name?.lexeme ?? '');
69
+ } else if (param is SimpleFormalParameter) {
70
+ constructorParams.add(param.name?.lexeme ?? '');
71
+ } else if (param is FieldFormalParameter) {
72
+ constructorParams.add(param.name.lexeme);
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ _classConstructorParams[className] = constructorParams;
79
+
80
+ super.visitClassDeclaration(node);
81
+
82
+ // After visiting all members, check if copyWith exists and compare
83
+ if (_classCopyWithMethods.containsKey(className)) {
84
+ final copyWithParams = _classCopyWithParams[className] ?? {};
85
+ final missingParams = constructorParams.difference(copyWithParams);
86
+
87
+ if (missingParams.isNotEmpty) {
88
+ final copyWithMethod = _classCopyWithMethods[className]!;
89
+ violations.add(analyzer.createViolation(
90
+ filePath: filePath,
91
+ line: analyzer.getLine(lineInfo, copyWithMethod.name.offset),
92
+ column: analyzer.getColumn(lineInfo, copyWithMethod.name.offset),
93
+ message:
94
+ 'copyWith method in class "$className" is missing parameters: ${missingParams.join(', ')}. All constructor parameters should be included in copyWith.',
95
+ ));
96
+ }
97
+ }
98
+
99
+ _currentClass = null;
100
+ }
101
+
102
+ @override
103
+ void visitMethodDeclaration(MethodDeclaration node) {
104
+ if (_currentClass != null && node.name.lexeme == 'copyWith') {
105
+ final className = _currentClass!.name.lexeme;
106
+ _classCopyWithMethods[className] = node;
107
+
108
+ // Collect copyWith parameters
109
+ final copyWithParams = <String>{};
110
+ if (node.parameters != null) {
111
+ for (final param in node.parameters!.parameters) {
112
+ if (param is DefaultFormalParameter) {
113
+ copyWithParams.add(param.parameter.name?.lexeme ?? '');
114
+ } else if (param is SimpleFormalParameter) {
115
+ copyWithParams.add(param.name?.lexeme ?? '');
116
+ }
117
+ }
118
+ }
119
+
120
+ _classCopyWithParams[className] = copyWithParams;
121
+ }
122
+
123
+ super.visitMethodDeclaration(node);
124
+ }
125
+ }
@@ -0,0 +1,134 @@
1
+ import 'dart:io';
2
+
3
+ import 'package:analyzer/dart/ast/ast.dart';
4
+ import 'package:analyzer/source/line_info.dart';
5
+ import 'package:path/path.dart' as path;
6
+
7
+ import '../../models/rule.dart';
8
+ import '../../models/violation.dart';
9
+ import '../base_analyzer.dart';
10
+
11
+ /// D016: Project Should Have Tests
12
+ /// Ensures the project has a test directory with test files
13
+ class D016ProjectShouldHaveTestsAnalyzer extends BaseAnalyzer {
14
+ @override
15
+ String get ruleId => 'D016';
16
+
17
+ // Default test directories to look for
18
+ static const _defaultTestDirectories = ['test', 'integration_test'];
19
+
20
+ // Test file pattern
21
+ static final _testFilePattern = RegExp(r'_test\.dart$');
22
+
23
+ // Track which projects we've already checked to avoid duplicate checks
24
+ static final Set<String> _checkedProjects = {};
25
+
26
+ @override
27
+ List<Violation> analyze({
28
+ required CompilationUnit unit,
29
+ required String filePath,
30
+ required Rule rule,
31
+ required LineInfo lineInfo,
32
+ }) {
33
+ final violations = <Violation>[];
34
+
35
+ // This rule checks for test directory existence, not individual Dart files
36
+ // We only need to check once per project
37
+ final projectRoot = _findProjectRoot(filePath);
38
+ if (projectRoot == null) {
39
+ return violations;
40
+ }
41
+
42
+ // Performance optimization: Skip if we've already checked this project
43
+ if (_checkedProjects.contains(projectRoot)) {
44
+ return violations;
45
+ }
46
+
47
+ // Mark as checked immediately
48
+ _checkedProjects.add(projectRoot);
49
+
50
+ // Get test directories from config, default to ['test', 'integration_test']
51
+ final testDirectories = (rule.config['testDirectories'] as List?)
52
+ ?.map((e) => e.toString())
53
+ .toList() ??
54
+ _defaultTestDirectories;
55
+
56
+ // Get minimum required test files from config, default to 1
57
+ final minTestFiles = (rule.config['minTestFiles'] as int?) ?? 1;
58
+
59
+ // Check if any test directory exists with test files
60
+ bool foundTestDirectory = false;
61
+ int totalTestFiles = 0;
62
+
63
+ for (final testDir in testDirectories) {
64
+ final testDirPath = path.join(projectRoot, testDir);
65
+ final testDirectory = Directory(testDirPath);
66
+
67
+ if (testDirectory.existsSync()) {
68
+ foundTestDirectory = true;
69
+
70
+ // Count test files in this directory
71
+ final testFiles = _findTestFiles(testDirectory);
72
+ totalTestFiles += testFiles.length;
73
+ }
74
+ }
75
+
76
+ // Report violations
77
+ if (!foundTestDirectory) {
78
+ violations.add(createViolation(
79
+ filePath: filePath,
80
+ line: 1,
81
+ column: 1,
82
+ message:
83
+ 'Project should have a test directory. Create a "test" directory with test files to ensure code quality and prevent regressions.',
84
+ ));
85
+ } else if (totalTestFiles < minTestFiles) {
86
+ violations.add(createViolation(
87
+ filePath: filePath,
88
+ line: 1,
89
+ column: 1,
90
+ message:
91
+ 'Project has a test directory but contains only $totalTestFiles test file(s). '
92
+ 'Add more test files (minimum: $minTestFiles) to properly test your code.',
93
+ ));
94
+ }
95
+
96
+ return violations;
97
+ }
98
+
99
+ /// Find the project root by looking for pubspec.yaml
100
+ String? _findProjectRoot(String filePath) {
101
+ var dir = Directory(path.dirname(filePath));
102
+
103
+ while (dir.path != dir.parent.path) {
104
+ final pubspec = File(path.join(dir.path, 'pubspec.yaml'));
105
+ if (pubspec.existsSync()) {
106
+ return dir.path;
107
+ }
108
+ dir = dir.parent;
109
+ }
110
+
111
+ return null;
112
+ }
113
+
114
+ /// Find all test files in a directory (recursively)
115
+ List<File> _findTestFiles(Directory directory) {
116
+ final testFiles = <File>[];
117
+
118
+ try {
119
+ final entities = directory.listSync(recursive: true);
120
+ for (final entity in entities) {
121
+ if (entity is File) {
122
+ final fileName = path.basename(entity.path);
123
+ if (_testFilePattern.hasMatch(fileName)) {
124
+ testFiles.add(entity);
125
+ }
126
+ }
127
+ }
128
+ } catch (e) {
129
+ // If we can't read the directory, return empty list
130
+ }
131
+
132
+ return testFiles;
133
+ }
134
+ }