@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,338 @@
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
+ /// D002: Always Dispose Resources and Remove Listeners
10
+ /// Ensures all resources (Controllers, StreamSubscriptions, FocusNodes) are properly disposed
11
+ ///
12
+ /// Important: Only checks owned resources (created locally), not external resources (from constructor)
13
+ class D002DisposeResourcesAnalyzer extends BaseAnalyzer {
14
+ @override
15
+ String get ruleId => 'D002';
16
+
17
+ // Types that require disposal (exact matches)
18
+ static const _disposableTypes = [
19
+ 'Controller',
20
+ 'StreamSubscription',
21
+ 'FocusNode',
22
+ 'AnimationController',
23
+ 'TextEditingController',
24
+ 'ScrollController',
25
+ 'TabController',
26
+ 'PageController',
27
+ 'VideoPlayerController',
28
+ 'CameraController',
29
+ 'ChangeNotifier',
30
+ 'ValueNotifier',
31
+ ];
32
+
33
+ // Pattern-based suffixes that commonly indicate disposable resources
34
+ static const _disposableSuffixes = [
35
+ 'Controller',
36
+ 'Disposable',
37
+ 'Subscription',
38
+ ];
39
+
40
+ // Methods that require corresponding cleanup
41
+ static const _methodPairsRequiringCleanup = {
42
+ 'subscribe': ['unsubscribe', 'cancel'],
43
+ 'addListener': ['removeListener'],
44
+ 'listen': ['cancel', 'pause'],
45
+ };
46
+
47
+ @override
48
+ List<Violation> analyze({
49
+ required CompilationUnit unit,
50
+ required String filePath,
51
+ required Rule rule,
52
+ required LineInfo lineInfo,
53
+ }) {
54
+ final violations = <Violation>[];
55
+
56
+ final visitor = _D002Visitor(
57
+ filePath: filePath,
58
+ lineInfo: lineInfo,
59
+ violations: violations,
60
+ analyzer: this,
61
+ );
62
+
63
+ unit.accept(visitor);
64
+
65
+ return violations;
66
+ }
67
+ }
68
+
69
+ class _D002Visitor extends RecursiveAstVisitor<void> {
70
+ final String filePath;
71
+ final LineInfo lineInfo;
72
+ final List<Violation> violations;
73
+ final D002DisposeResourcesAnalyzer analyzer;
74
+
75
+ // Track disposable resources per class
76
+ final Map<String, List<_DisposableResource>> _disposableResourcesByClass = {};
77
+ // Track resources created by method calls (subscribe, addListener, etc.)
78
+ final Map<String, List<_DisposableResource>> _methodBasedResourcesByClass = {};
79
+ String? _currentClassName;
80
+
81
+ _D002Visitor({
82
+ required this.filePath,
83
+ required this.lineInfo,
84
+ required this.violations,
85
+ required this.analyzer,
86
+ });
87
+
88
+ @override
89
+ void visitClassDeclaration(ClassDeclaration node) {
90
+ final className = node.name.lexeme;
91
+ _methodBasedResourcesByClass[className] = [];
92
+ _currentClassName = className;
93
+ _disposableResourcesByClass[className] = [];
94
+
95
+ // First: collect external resources from constructor parameters
96
+ final externalResources = <String>{};
97
+ for (final member in node.members) {
98
+ if (member is ConstructorDeclaration) {
99
+ for (final param in member.parameters.parameters) {
100
+ if (param is FieldFormalParameter) {
101
+ // this.controller pattern - external resource
102
+ externalResources.add(param.name.lexeme);
103
+ } else if (param is SimpleFormalParameter) {
104
+ // Check if it's assigned to a field in initializers
105
+ final paramName = param.name?.lexeme;
106
+ if (paramName != null) {
107
+ // Check initializer list for field assignments
108
+ if (member.initializers.any((init) {
109
+ if (init is ConstructorFieldInitializer) {
110
+ return init.expression.toSource().contains(paramName);
111
+ }
112
+ return false;
113
+ })) {
114
+ externalResources.add(paramName);
115
+ }
116
+ }
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ // Second: collect all disposable fields (only owned ones)
123
+ for (final member in node.members) {
124
+ if (member is FieldDeclaration) {
125
+ _visitFieldForDisposableResources(member, externalResources);
126
+ }
127
+ }
128
+
129
+ // Second pass: check if dispose method exists and handles all resources
130
+ MethodDeclaration? disposeMethod;
131
+ for (final member in node.members) {
132
+ if (member is MethodDeclaration && member.name.lexeme == 'dispose') {
133
+ disposeMethod = member;
134
+ break;
135
+ }
136
+ }
137
+
138
+ final disposableResources = _disposableResourcesByClass[className] ?? [];
139
+ final methodBasedResources = _methodBasedResourcesByClass[className] ?? [];
140
+ final allResources = [...disposableResources, ...methodBasedResources];
141
+
142
+ if (allResources.isNotEmpty) {
143
+ if (disposeMethod == null) {
144
+ // No dispose method found but there are disposable resources
145
+ violations.add(analyzer.createViolation(
146
+ filePath: filePath,
147
+ line: analyzer.getLine(lineInfo, node.name.offset),
148
+ column: analyzer.getColumn(lineInfo, node.name.offset),
149
+ message:
150
+ 'Class "$className" has ${allResources.length} owned disposable resource(s) but no dispose() method. '
151
+ 'Resources: ${allResources.map((r) => r.name).join(", ")}',
152
+ ));
153
+ } else {
154
+ // Check if all resources are disposed
155
+ _checkDisposeMethodCompleteness(
156
+ disposeMethod,
157
+ allResources,
158
+ className,
159
+ );
160
+ }
161
+ }
162
+
163
+ super.visitClassDeclaration(node);
164
+ _currentClassName = null;
165
+ }
166
+
167
+ void _visitFieldForDisposableResources(
168
+ FieldDeclaration node,
169
+ Set<String> externalResources,
170
+ ) {
171
+ final type = node.fields.type?.toSource() ?? '';
172
+
173
+ for (final variable in node.fields.variables) {
174
+ final variableName = variable.name.lexeme;
175
+
176
+ // Skip external resources (from constructor parameters)
177
+ if (externalResources.contains(variableName)) {
178
+ continue;
179
+ }
180
+
181
+ // Skip if no initializer (might be late-initialized or constructor-assigned external resource)
182
+ if (variable.initializer == null && node.fields.isLate) {
183
+ continue;
184
+ }
185
+
186
+ // Determine the type to check
187
+ String typeToCheck = type;
188
+ if (typeToCheck.isEmpty && variable.initializer != null) {
189
+ // No explicit type annotation, infer from initializer
190
+ // e.g., final controller = TextEditingController()
191
+ final initializerStr = variable.initializer!.toSource();
192
+ // Extract constructor name (everything before the first '(')
193
+ final match = RegExp(r'(\w+)\s*\(').firstMatch(initializerStr);
194
+ if (match != null) {
195
+ typeToCheck = match.group(1) ?? '';
196
+ }
197
+ }
198
+
199
+ // Check if the type requires disposal
200
+ if (_isDisposableType(typeToCheck)) {
201
+ // Only track if it has an initializer (owned resource)
202
+ if (variable.initializer != null) {
203
+ _disposableResourcesByClass[_currentClassName]?.add(
204
+ _DisposableResource(
205
+ name: variableName,
206
+ type: typeToCheck,
207
+ line: analyzer.getLine(lineInfo, variable.name.offset),
208
+ column: analyzer.getColumn(lineInfo, variable.name.offset),
209
+ isOwned: true,
210
+ ),
211
+ );
212
+ }
213
+ }
214
+
215
+ // Check if the initialization calls methods that require cleanup
216
+ if (variable.initializer != null) {
217
+ _checkMethodBasedInitialization(variable);
218
+ }
219
+ }
220
+ }
221
+
222
+ void _checkMethodBasedInitialization(VariableDeclaration variable) {
223
+ final initializer = variable.initializer;
224
+ if (initializer == null) return;
225
+
226
+ final initializerSource = initializer.toSource();
227
+ final variableName = variable.name.lexeme;
228
+
229
+ // Check if initialization calls methods requiring cleanup
230
+ for (final entry in D002DisposeResourcesAnalyzer._methodPairsRequiringCleanup.entries) {
231
+ final setupMethod = entry.key;
232
+
233
+ if (initializerSource.contains('.$setupMethod(')) {
234
+ _methodBasedResourcesByClass[_currentClassName]?.add(
235
+ _DisposableResource(
236
+ name: variableName,
237
+ type: 'Method-based ($setupMethod)',
238
+ line: analyzer.getLine(lineInfo, variable.name.offset),
239
+ column: analyzer.getColumn(lineInfo, variable.name.offset),
240
+ requiresMethodCleanup: setupMethod,
241
+ isOwned: true,
242
+ ),
243
+ );
244
+ break;
245
+ }
246
+ }
247
+ }
248
+
249
+ bool _isDisposableType(String type) {
250
+ // Remove generic parameters and null-safety markers
251
+ final cleanType = type.replaceAll(RegExp(r'[<>?].*'), '').trim();
252
+
253
+ // Check exact type matches (known Flutter/Dart types)
254
+ final hasExactMatch = D002DisposeResourcesAnalyzer._disposableTypes.any(
255
+ (disposableType) => cleanType.contains(disposableType),
256
+ );
257
+
258
+ if (hasExactMatch) {
259
+ return true;
260
+ }
261
+
262
+ // Check pattern-based suffixes for custom disposable types
263
+ // e.g., CustomController, AppStateDisposable, UserNotifier, etc.
264
+ final hasSuffixMatch = D002DisposeResourcesAnalyzer._disposableSuffixes.any(
265
+ (suffix) => cleanType.endsWith(suffix),
266
+ );
267
+
268
+ return hasSuffixMatch;
269
+ }
270
+
271
+ void _checkDisposeMethodCompleteness(
272
+ MethodDeclaration disposeMethod,
273
+ List<_DisposableResource> resources,
274
+ String className,
275
+ ) {
276
+ final disposeBody = disposeMethod.body.toSource();
277
+ final undisposedResources = <_DisposableResource>[];
278
+
279
+ for (final resource in resources) {
280
+ // Check if resource is disposed or cancelled
281
+ final isDisposed = _isResourceDisposed(disposeBody, resource.name);
282
+
283
+ if (!isDisposed) {
284
+ undisposedResources.add(resource);
285
+ }
286
+ }
287
+
288
+ // Report violations for undisposed resources
289
+ for (final resource in undisposedResources) {
290
+ violations.add(analyzer.createViolation(
291
+ filePath: filePath,
292
+ line: resource.line,
293
+ column: resource.column,
294
+ message:
295
+ 'Resource "${resource.name}" of type "${resource.type}" is not disposed in dispose() method. '
296
+ 'Add "${resource.name}.dispose()" or "${resource.name}.cancel()" in the dispose() method.',
297
+ ));
298
+ }
299
+ }
300
+
301
+ bool _isResourceDisposed(String disposeBody, String resourceName) {
302
+ // Common disposal patterns
303
+ final disposalPatterns = [
304
+ '$resourceName.dispose()',
305
+ '$resourceName?.dispose()',
306
+ '$resourceName.cancel()',
307
+ '$resourceName?.cancel()',
308
+ '$resourceName.close()',
309
+ '$resourceName?.close()',
310
+ '$resourceName.unsubscribe()',
311
+ '$resourceName?.unsubscribe()',
312
+ '$resourceName.removeListener(',
313
+ '$resourceName?.removeListener(',
314
+ // Check for null assignment (not ideal but acceptable)
315
+ '$resourceName = null',
316
+ ];
317
+
318
+ return disposalPatterns.any((pattern) => disposeBody.contains(pattern));
319
+ }
320
+ }
321
+
322
+ class _DisposableResource {
323
+ final String name;
324
+ final String type;
325
+ final int line;
326
+ final int column;
327
+ final String? requiresMethodCleanup; // e.g., 'subscribe', 'addListener'
328
+ final bool isOwned; // true if created locally, false if external
329
+
330
+ _DisposableResource({
331
+ required this.name,
332
+ required this.type,
333
+ required this.line,
334
+ required this.column,
335
+ this.requiresMethodCleanup,
336
+ this.isOwned = true,
337
+ });
338
+ }
@@ -0,0 +1,273 @@
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
+ /// D003: Prefer Widgets Over Methods Returning Widgets
10
+ /// Recommends extracting methods that return widgets into separate widget classes
11
+ /// for better performance, reusability, and maintainability
12
+ class D003PreferWidgetsOverMethodsAnalyzer extends BaseAnalyzer {
13
+ @override
14
+ String get ruleId => 'D003';
15
+
16
+ // Widget types to check for in return types
17
+ // Includes base types and common Flutter widgets
18
+ static const _widgetTypes = [
19
+ 'Widget',
20
+ 'StatelessWidget',
21
+ 'StatefulWidget',
22
+ 'PreferredSizeWidget',
23
+ 'InheritedWidget',
24
+ 'Container',
25
+ 'Text',
26
+ 'Column',
27
+ 'Row',
28
+ 'Stack',
29
+ 'Scaffold',
30
+ 'AppBar',
31
+ 'ListView',
32
+ 'GridView',
33
+ 'Card',
34
+ 'Padding',
35
+ 'Center',
36
+ 'SizedBox',
37
+ 'Expanded',
38
+ 'Flexible',
39
+ 'CustomPaint',
40
+ 'CustomScrollView',
41
+ 'Align',
42
+ 'AspectRatio',
43
+ 'Baseline',
44
+ 'ConstrainedBox',
45
+ 'FittedBox',
46
+ 'FractionallySizedBox',
47
+ 'LimitedBox',
48
+ 'Offstage',
49
+ 'OverflowBox',
50
+ 'SizedOverflowBox',
51
+ 'Transform',
52
+ 'Wrap',
53
+ ];
54
+
55
+ @override
56
+ List<Violation> analyze({
57
+ required CompilationUnit unit,
58
+ required String filePath,
59
+ required Rule rule,
60
+ required LineInfo lineInfo,
61
+ }) {
62
+ final violations = <Violation>[];
63
+
64
+ final visitor = _D003Visitor(
65
+ filePath: filePath,
66
+ lineInfo: lineInfo,
67
+ violations: violations,
68
+ analyzer: this,
69
+ );
70
+
71
+ unit.accept(visitor);
72
+
73
+ return violations;
74
+ }
75
+ }
76
+
77
+ class _D003Visitor extends RecursiveAstVisitor<void> {
78
+ final String filePath;
79
+ final LineInfo lineInfo;
80
+ final List<Violation> violations;
81
+ final D003PreferWidgetsOverMethodsAnalyzer analyzer;
82
+
83
+ String? _currentClassName;
84
+ bool _insideWidgetClass = false;
85
+
86
+ _D003Visitor({
87
+ required this.filePath,
88
+ required this.lineInfo,
89
+ required this.violations,
90
+ required this.analyzer,
91
+ });
92
+
93
+ @override
94
+ void visitClassDeclaration(ClassDeclaration node) {
95
+ final previousClassName = _currentClassName;
96
+ final previousInsideWidgetClass = _insideWidgetClass;
97
+
98
+ _currentClassName = node.name.lexeme;
99
+
100
+ // Check if this class extends a Widget
101
+ final extendsClause = node.extendsClause;
102
+ if (extendsClause != null) {
103
+ final superclass = extendsClause.superclass.name2.lexeme;
104
+ _insideWidgetClass = superclass == 'StatelessWidget' ||
105
+ superclass == 'StatefulWidget' ||
106
+ superclass.contains('Widget');
107
+ }
108
+
109
+ super.visitClassDeclaration(node);
110
+
111
+ _currentClassName = previousClassName;
112
+ _insideWidgetClass = previousInsideWidgetClass;
113
+ }
114
+
115
+ @override
116
+ void visitMethodDeclaration(MethodDeclaration node) {
117
+ // Only check methods inside widget classes
118
+ if (!_insideWidgetClass) {
119
+ super.visitMethodDeclaration(node);
120
+ return;
121
+ }
122
+
123
+ // Skip the build method itself
124
+ final methodName = node.name.lexeme;
125
+ if (methodName == 'build') {
126
+ super.visitMethodDeclaration(node);
127
+ return;
128
+ }
129
+
130
+ // Skip lifecycle methods
131
+ if (_isLifecycleMethod(methodName)) {
132
+ super.visitMethodDeclaration(node);
133
+ return;
134
+ }
135
+
136
+ // Check if method returns a Widget type
137
+ final returnType = node.returnType?.toSource();
138
+ if (returnType == null) {
139
+ super.visitMethodDeclaration(node);
140
+ return;
141
+ }
142
+
143
+ if (_isWidgetType(returnType)) {
144
+ // This is a method returning a widget - flag it
145
+ final isPrivate = methodName.startsWith('_');
146
+ final suggestion = isPrivate
147
+ ? 'Extract "$methodName" into a private StatelessWidget class'
148
+ : 'Extract "$methodName" into a StatelessWidget class';
149
+
150
+ violations.add(analyzer.createViolation(
151
+ filePath: filePath,
152
+ line: analyzer.getLine(lineInfo, node.name.offset),
153
+ column: analyzer.getColumn(lineInfo, node.name.offset),
154
+ message:
155
+ 'Method "$methodName" returns a widget. $suggestion for better performance and maintainability. '
156
+ 'Widget rebuilds will be optimized, and the code will be more reusable.',
157
+ ));
158
+ }
159
+
160
+ super.visitMethodDeclaration(node);
161
+ }
162
+
163
+ @override
164
+ void visitNamedExpression(NamedExpression node) {
165
+ // Check for method calls in child: or children: parameters
166
+ final paramName = node.name.label.name;
167
+
168
+ if (paramName == 'child' || paramName == 'children') {
169
+ // Check if the value is a method invocation
170
+ final expression = node.expression;
171
+
172
+ if (expression is MethodInvocation) {
173
+ final methodName = expression.methodName.name;
174
+
175
+ // Flag methods that look like they build widgets
176
+ // Common patterns: _build*, build*, _create*, create*, _get*Widget
177
+ if (_looksLikeWidgetBuilderMethod(methodName)) {
178
+ violations.add(analyzer.createViolation(
179
+ filePath: filePath,
180
+ line: analyzer.getLine(lineInfo, expression.methodName.offset),
181
+ column: analyzer.getColumn(lineInfo, expression.methodName.offset),
182
+ message:
183
+ 'Method call "$methodName()" in $paramName: parameter. '
184
+ 'Extract this into a proper widget class for better performance. '
185
+ 'Widget methods called in child/children parameters prevent optimization.',
186
+ ));
187
+ }
188
+ } else if (expression is ListLiteral) {
189
+ // Check for method calls inside children: list
190
+ for (final element in expression.elements) {
191
+ if (element is MethodInvocation) {
192
+ final methodName = element.methodName.name;
193
+ if (_looksLikeWidgetBuilderMethod(methodName)) {
194
+ violations.add(analyzer.createViolation(
195
+ filePath: filePath,
196
+ line: analyzer.getLine(lineInfo, element.methodName.offset),
197
+ column: analyzer.getColumn(lineInfo, element.methodName.offset),
198
+ message:
199
+ 'Method call "$methodName()" in children: list. '
200
+ 'Extract this into a proper widget class for better performance.',
201
+ ));
202
+ }
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ super.visitNamedExpression(node);
209
+ }
210
+
211
+ bool _looksLikeWidgetBuilderMethod(String methodName) {
212
+ // Check if method name follows common widget builder patterns
213
+ return methodName.startsWith('_build') ||
214
+ methodName.startsWith('build') ||
215
+ methodName.startsWith('_create') ||
216
+ methodName.startsWith('create') ||
217
+ methodName.startsWith('_get') && methodName.contains('Widget') ||
218
+ methodName.startsWith('get') && methodName.contains('Widget');
219
+ }
220
+
221
+ bool _isWidgetType(String type) {
222
+ // Remove generics and null-safety markers
223
+ final cleanType = type.replaceAll(RegExp(r'[<>?].*'), '').trim();
224
+
225
+ // Strategy 1: Check if it's a known widget type
226
+ final isKnownWidget = D003PreferWidgetsOverMethodsAnalyzer._widgetTypes
227
+ .any((widgetType) => cleanType == widgetType || cleanType.endsWith(widgetType));
228
+
229
+ if (isKnownWidget) {
230
+ return true;
231
+ }
232
+
233
+ // Strategy 2: Check if it ends with 'Widget' (catches custom widgets)
234
+ // e.g., CustomWidget, UserProfileWidget, MyAppWidget
235
+ if (cleanType.endsWith('Widget')) {
236
+ return true;
237
+ }
238
+
239
+ // Strategy 3: Check if it ends with common widget suffixes
240
+ // e.g., CustomPainter, CustomClipper, CustomCard, CustomButton
241
+ const customWidgetSuffixes = [
242
+ 'Painter', // CustomPainter
243
+ 'Clipper', // CustomClipper
244
+ 'Card', // CustomCard
245
+ 'Button', // CustomButton
246
+ 'Dialog', // CustomDialog
247
+ 'Sheet', // CustomSheet
248
+ 'Bar', // CustomBar, TabBar
249
+ 'View', // CustomView, GridView
250
+ 'List', // CustomList
251
+ 'Item', // ListItem, GridItem
252
+ 'Tile', // ListTile, CustomTile
253
+ ];
254
+
255
+ return customWidgetSuffixes.any((suffix) => cleanType.endsWith(suffix));
256
+ }
257
+
258
+ bool _isLifecycleMethod(String methodName) {
259
+ // Skip Flutter lifecycle methods
260
+ const lifecycleMethods = [
261
+ 'initState',
262
+ 'dispose',
263
+ 'didChangeDependencies',
264
+ 'didUpdateWidget',
265
+ 'reassemble',
266
+ 'deactivate',
267
+ 'setState',
268
+ 'createState',
269
+ ];
270
+
271
+ return lifecycleMethods.contains(methodName);
272
+ }
273
+ }