@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,187 @@
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
+ /// D017: Pubspec Dependencies Should Be Reviewed Regularly
12
+ /// Ensures dependencies in pubspec.yaml are reviewed and updated regularly
13
+ class D017PubspecDependenciesReviewAnalyzer extends BaseAnalyzer {
14
+ @override
15
+ String get ruleId => 'D017';
16
+
17
+ // Default maximum months without review
18
+ static const int _defaultMaxMonths = 4;
19
+
20
+ // Track which projects we've already reported on to avoid duplicate warnings
21
+ static final Set<String> _reportedProjects = {};
22
+
23
+ @override
24
+ List<Violation> analyze({
25
+ required CompilationUnit unit,
26
+ required String filePath,
27
+ required Rule rule,
28
+ required LineInfo lineInfo,
29
+ }) {
30
+ final violations = <Violation>[];
31
+
32
+ // This rule checks pubspec.yaml/pubspec.lock modification dates
33
+ final projectRoot = _findProjectRoot(filePath);
34
+ if (projectRoot == null) {
35
+ return violations;
36
+ }
37
+
38
+ // Performance optimization: Skip if we've already checked this project
39
+ // This prevents analyzing every Dart file when we only need to check once
40
+ if (_reportedProjects.contains(projectRoot)) {
41
+ return violations;
42
+ }
43
+
44
+ // Get configuration
45
+ final maxMonths = (rule.config['maxMonthsWithoutReview'] as int?) ?? _defaultMaxMonths;
46
+ final checkLockFile = (rule.config['checkLockFile'] as bool?) ?? true;
47
+
48
+ // Check pubspec.yaml
49
+ final pubspecPath = path.join(projectRoot, 'pubspec.yaml');
50
+ final pubspecFile = File(pubspecPath);
51
+
52
+ if (!pubspecFile.existsSync()) {
53
+ // No pubspec.yaml - this is unusual but not our concern here
54
+ return violations;
55
+ }
56
+
57
+ // Check pubspec.lock if configured
58
+ final lockFilePath = path.join(projectRoot, 'pubspec.lock');
59
+ final lockFile = File(lockFilePath);
60
+
61
+ DateTime? lastModified;
62
+ String checkedFile = 'pubspec.yaml';
63
+ bool usedGit = false;
64
+
65
+ if (checkLockFile && lockFile.existsSync()) {
66
+ // Prefer checking lock file as it reflects actual resolved dependencies
67
+ checkedFile = 'pubspec.lock';
68
+ // Try to get last commit date from git first
69
+ lastModified = _getGitLastModifiedDate(lockFilePath, projectRoot);
70
+ if (lastModified != null) {
71
+ usedGit = true;
72
+ } else {
73
+ // Fall back to file system timestamp
74
+ lastModified = lockFile.lastModifiedSync();
75
+ }
76
+ } else {
77
+ // Fall back to pubspec.yaml
78
+ lastModified = _getGitLastModifiedDate(pubspecPath, projectRoot);
79
+ if (lastModified != null) {
80
+ usedGit = true;
81
+ } else {
82
+ lastModified = pubspecFile.lastModifiedSync();
83
+ }
84
+ }
85
+
86
+ // Calculate months since last update
87
+ final now = DateTime.now();
88
+ final monthsSinceUpdate = _calculateMonthsDifference(lastModified, now);
89
+
90
+ // Mark this project as checked (do this before checking threshold)
91
+ _reportedProjects.add(projectRoot);
92
+
93
+ if (monthsSinceUpdate > maxMonths) {
94
+ final dateSource = usedGit ? 'git commit' : 'file system';
95
+ violations.add(createViolation(
96
+ filePath: pubspecPath,
97
+ line: 1,
98
+ column: 1,
99
+ message:
100
+ 'Dependencies have not been reviewed for $monthsSinceUpdate months '
101
+ '(last updated: ${_formatDate(lastModified)}). '
102
+ 'Review and update dependencies regularly to ensure security patches and bug fixes are applied. '
103
+ 'Threshold: $maxMonths months. File checked: $checkedFile (source: $dateSource)',
104
+ ));
105
+ }
106
+
107
+ return violations;
108
+ }
109
+
110
+ /// Find the project root by looking for pubspec.yaml
111
+ String? _findProjectRoot(String filePath) {
112
+ var dir = Directory(path.dirname(filePath));
113
+
114
+ while (dir.path != dir.parent.path) {
115
+ final pubspec = File(path.join(dir.path, 'pubspec.yaml'));
116
+ if (pubspec.existsSync()) {
117
+ return dir.path;
118
+ }
119
+ dir = dir.parent;
120
+ }
121
+
122
+ return null;
123
+ }
124
+
125
+ /// Calculate the difference in months between two dates
126
+ int _calculateMonthsDifference(DateTime start, DateTime end) {
127
+ final yearsDiff = end.year - start.year;
128
+ final monthsDiff = end.month - start.month;
129
+ final totalMonths = (yearsDiff * 12) + monthsDiff;
130
+
131
+ // If we're past the same day in the month, count it as a full month
132
+ // Otherwise, don't count the current partial month
133
+ if (end.day >= start.day) {
134
+ return totalMonths;
135
+ } else {
136
+ return totalMonths - 1;
137
+ }
138
+ }
139
+
140
+ /// Format date for display
141
+ String _formatDate(DateTime date) {
142
+ return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
143
+ }
144
+
145
+ /// Get last modified date from git commit history
146
+ /// Returns null if git is not available or command fails
147
+ DateTime? _getGitLastModifiedDate(String filePath, String projectRoot) {
148
+ try {
149
+ // Use git log to get the last commit date for this file
150
+ // %ci = committer date in ISO 8601 format
151
+ final result = Process.runSync(
152
+ 'git',
153
+ ['log', '-1', '--format=%ci', '--', filePath],
154
+ workingDirectory: projectRoot,
155
+ runInShell: true,
156
+ );
157
+
158
+ if (result.exitCode == 0) {
159
+ final output = (result.stdout as String).trim();
160
+ if (output.isNotEmpty) {
161
+ // Parse git date format: "2024-01-15 10:30:45 +0700"
162
+ // We only need the date part
163
+ final datePart = output.split(' ')[0]; // "2024-01-15"
164
+ final timePart = output.split(' ')[1]; // "10:30:45"
165
+
166
+ final dateComponents = datePart.split('-');
167
+ final timeComponents = timePart.split(':');
168
+
169
+ if (dateComponents.length == 3 && timeComponents.length == 3) {
170
+ return DateTime(
171
+ int.parse(dateComponents[0]), // year
172
+ int.parse(dateComponents[1]), // month
173
+ int.parse(dateComponents[2]), // day
174
+ int.parse(timeComponents[0]), // hour
175
+ int.parse(timeComponents[1]), // minute
176
+ int.parse(timeComponents[2]), // second
177
+ );
178
+ }
179
+ }
180
+ }
181
+ } catch (e) {
182
+ // Git command failed or not available, will fall back to file system
183
+ }
184
+
185
+ return null;
186
+ }
187
+ }
@@ -0,0 +1,196 @@
1
+ import 'package:analyzer/dart/ast/ast.dart';
2
+ import 'package:analyzer/source/line_info.dart';
3
+ import 'package:analyzer/dart/ast/token.dart';
4
+
5
+ import '../../models/rule.dart';
6
+ import '../../models/violation.dart';
7
+ import '../base_analyzer.dart';
8
+
9
+ /// D018: Remove Commented-Out Code
10
+ /// Remove commented-out code instead of leaving it in source. Use version control to track history.
11
+ class D018RemoveCommentedCodeAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'D018';
14
+
15
+ // Default minimum lines of consecutive commented code to flag
16
+ static const int _defaultMinLines = 2;
17
+
18
+ @override
19
+ List<Violation> analyze({
20
+ required CompilationUnit unit,
21
+ required String filePath,
22
+ required Rule rule,
23
+ required LineInfo lineInfo,
24
+ }) {
25
+ final violations = <Violation>[];
26
+
27
+ // Get config values
28
+ final minLines = (rule.config['minLines'] as int?) ?? _defaultMinLines;
29
+ final ignoreDocComments = (rule.config['ignoreDocComments'] as bool?) ?? true;
30
+
31
+ // Analyze comments from tokens
32
+ _analyzeComments(
33
+ unit: unit,
34
+ filePath: filePath,
35
+ lineInfo: lineInfo,
36
+ violations: violations,
37
+ minLines: minLines,
38
+ ignoreDocComments: ignoreDocComments,
39
+ );
40
+
41
+ return violations;
42
+ }
43
+
44
+ void _analyzeComments({
45
+ required CompilationUnit unit,
46
+ required String filePath,
47
+ required LineInfo lineInfo,
48
+ required List<Violation> violations,
49
+ required int minLines,
50
+ required bool ignoreDocComments,
51
+ }) {
52
+ final Token? firstToken = unit.beginToken;
53
+ if (firstToken == null) return;
54
+
55
+ // Process all tokens to find comments
56
+ Token? token = firstToken;
57
+ while (token != null) {
58
+ // Check preceding comments
59
+ Token? comment = token.precedingComments;
60
+ List<Token> commentBlock = [];
61
+
62
+ while (comment != null) {
63
+ // Skip doc comments if configured
64
+ if (ignoreDocComments && _isDocComment(comment)) {
65
+ comment = comment.next;
66
+ continue;
67
+ }
68
+
69
+ // Add all consecutive comments to the block
70
+ commentBlock.add(comment);
71
+ comment = comment.next;
72
+ }
73
+
74
+ // Analyze the comment block for code patterns
75
+ if (commentBlock.isNotEmpty) {
76
+ _analyzeCommentBlock(
77
+ violations: violations,
78
+ filePath: filePath,
79
+ lineInfo: lineInfo,
80
+ commentBlock: commentBlock,
81
+ minLines: minLines,
82
+ );
83
+ }
84
+
85
+ if (token.type == TokenType.EOF) break;
86
+ token = token.next;
87
+ }
88
+ }
89
+
90
+ void _analyzeCommentBlock({
91
+ required List<Violation> violations,
92
+ required String filePath,
93
+ required LineInfo lineInfo,
94
+ required List<Token> commentBlock,
95
+ required int minLines,
96
+ }) {
97
+ if (commentBlock.isEmpty) return;
98
+
99
+ // Count how many comments look like code
100
+ int codeCommentCount = 0;
101
+ for (final comment in commentBlock) {
102
+ if (_looksLikeCommentedCode(comment)) {
103
+ codeCommentCount++;
104
+ }
105
+ }
106
+
107
+ // If majority of comments (>70%) look like code, flag the whole block
108
+ if (codeCommentCount >= minLines &&
109
+ codeCommentCount >= (commentBlock.length * 0.7).ceil()) {
110
+ _addViolation(
111
+ violations: violations,
112
+ filePath: filePath,
113
+ lineInfo: lineInfo,
114
+ commentBlock: commentBlock,
115
+ );
116
+ }
117
+ }
118
+
119
+ bool _isDocComment(Token comment) {
120
+ final text = comment.lexeme;
121
+ return text.startsWith('///') || text.startsWith('/**');
122
+ }
123
+
124
+ bool _looksLikeCommentedCode(Token comment) {
125
+ final text = comment.lexeme.trim();
126
+
127
+ // Remove comment markers
128
+ String content = text;
129
+ if (content.startsWith('//')) {
130
+ content = content.substring(2).trim();
131
+ } else if (content.startsWith('/*')) {
132
+ content = content.substring(2);
133
+ if (content.endsWith('*/')) {
134
+ content = content.substring(0, content.length - 2);
135
+ }
136
+ content = content.trim();
137
+ }
138
+
139
+ // Empty or very short comments are likely not code
140
+ if (content.length < 3) return false;
141
+
142
+ // Patterns that suggest commented code
143
+ final codePatterns = [
144
+ // Dart/Flutter specific
145
+ RegExp(r'\b(var|final|const|late|void|int|String|bool|double|List|Map|Set|Future|Stream|Widget)\b'),
146
+ RegExp(r'\b(if|else|for|while|switch|case|break|continue|return|throw|try|catch|finally)\b'),
147
+ RegExp(r'\b(class|extends|implements|mixin|with|abstract|interface)\b'),
148
+ RegExp(r'\b(import|export|part|library|show|hide|as)\b'),
149
+ RegExp(r'\b(async|await|yield|sync)\b'),
150
+ RegExp(r'\b(this|super|static|override|factory|operator)\b'),
151
+ RegExp(r'\b(get|set|enum|typedef)\b'),
152
+
153
+ // Common code structures
154
+ RegExp(r'^[a-zA-Z_]\w*\s*\(.*\)\s*[{;]?$'), // function calls/declarations
155
+ RegExp(r'^[a-zA-Z_]\w*\s*='), // assignments
156
+ RegExp(r'[{}\[\]()]'), // brackets
157
+ RegExp(r';$'), // ends with semicolon
158
+ RegExp(r'=>'), // arrow functions
159
+ RegExp(r'\?\?|\?\.|\?|\!'), // null safety operators
160
+ RegExp(r'[+\-*/%&|^]?='), // compound assignments
161
+ RegExp(r'<.*>'), // generics
162
+ RegExp(r'@\w+'), // annotations
163
+ RegExp(r'\.\.'), // cascade operator
164
+ ];
165
+
166
+ // Check if the content matches any code pattern
167
+ for (final pattern in codePatterns) {
168
+ if (pattern.hasMatch(content)) {
169
+ return true;
170
+ }
171
+ }
172
+
173
+ return false;
174
+ }
175
+
176
+ void _addViolation({
177
+ required List<Violation> violations,
178
+ required String filePath,
179
+ required LineInfo lineInfo,
180
+ required List<Token> commentBlock,
181
+ }) {
182
+ if (commentBlock.isEmpty) return;
183
+
184
+ final firstComment = commentBlock.first;
185
+ final line = getLine(lineInfo, firstComment.offset);
186
+ final column = getColumn(lineInfo, firstComment.offset);
187
+ final count = commentBlock.length;
188
+
189
+ violations.add(createViolation(
190
+ filePath: filePath,
191
+ line: line,
192
+ column: column,
193
+ message: 'Found $count line${count > 1 ? 's' : ''} of commented-out code. Remove it and use version control instead.',
194
+ ));
195
+ }
196
+ }
@@ -0,0 +1,161 @@
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
+ /// D019: Avoid Single Child in Multi-Child Widget
10
+ /// Multi-child widgets (Column, Row, Wrap, etc.) should not have only a single child
11
+ class D019AvoidSingleChildMultiChildWidgetAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'D019';
14
+
15
+ // Multi-child widgets that should have multiple children
16
+ static const List<String> _multiChildWidgets = [
17
+ 'Column',
18
+ 'Row',
19
+ 'Wrap',
20
+ 'Stack',
21
+ 'Flex',
22
+ 'ListView',
23
+ 'GridView',
24
+ 'CustomScrollView',
25
+ ];
26
+
27
+ @override
28
+ List<Violation> analyze({
29
+ required CompilationUnit unit,
30
+ required String filePath,
31
+ required Rule rule,
32
+ required LineInfo lineInfo,
33
+ }) {
34
+ final violations = <Violation>[];
35
+
36
+ final visitor = _D019Visitor(
37
+ filePath: filePath,
38
+ lineInfo: lineInfo,
39
+ violations: violations,
40
+ analyzer: this,
41
+ );
42
+
43
+ unit.accept(visitor);
44
+
45
+ return violations;
46
+ }
47
+ }
48
+
49
+ class _D019Visitor extends RecursiveAstVisitor<void> {
50
+ final String filePath;
51
+ final LineInfo lineInfo;
52
+ final List<Violation> violations;
53
+ final D019AvoidSingleChildMultiChildWidgetAnalyzer analyzer;
54
+
55
+ _D019Visitor({
56
+ required this.filePath,
57
+ required this.lineInfo,
58
+ required this.violations,
59
+ required this.analyzer,
60
+ });
61
+
62
+ @override
63
+ void visitInstanceCreationExpression(InstanceCreationExpression node) {
64
+ final typeName = node.constructorName.type.toSource();
65
+
66
+ // Check if this is a multi-child widget
67
+ final isMultiChildWidget = D019AvoidSingleChildMultiChildWidgetAnalyzer._multiChildWidgets
68
+ .any((widget) => typeName == widget || typeName.startsWith('$widget.'));
69
+
70
+ if (isMultiChildWidget) {
71
+ _checkChildrenCountFromInstance(node, typeName);
72
+ }
73
+
74
+ super.visitInstanceCreationExpression(node);
75
+ }
76
+
77
+ @override
78
+ void visitMethodInvocation(MethodInvocation node) {
79
+ // In parsed AST (without resolution), widget constructors like Column()
80
+ // are represented as MethodInvocation instead of InstanceCreationExpression
81
+ // We need to handle both cases to work with getParsedUnit()
82
+
83
+ final methodName = node.methodName.name;
84
+
85
+ // Handle direct widget constructor calls (no target, starts with uppercase)
86
+ if (node.target == null && methodName.isNotEmpty) {
87
+ final isMultiChildWidget = D019AvoidSingleChildMultiChildWidgetAnalyzer._multiChildWidgets
88
+ .contains(methodName);
89
+
90
+ if (isMultiChildWidget) {
91
+ _checkChildrenCountFromInvocation(node, methodName);
92
+ }
93
+ }
94
+
95
+ super.visitMethodInvocation(node);
96
+ }
97
+
98
+ void _checkChildrenCountFromInstance(InstanceCreationExpression node, String typeName) {
99
+ final arguments = node.argumentList.arguments;
100
+
101
+ for (final arg in arguments) {
102
+ if (arg is NamedExpression) {
103
+ final paramName = arg.name.label.name;
104
+
105
+ // Check for 'children' or 'slivers' parameter
106
+ if (paramName == 'children' || paramName == 'slivers') {
107
+ final expression = arg.expression;
108
+
109
+ // Check if it's a list literal
110
+ if (expression is ListLiteral) {
111
+ final elementCount = expression.elements.length;
112
+
113
+ if (elementCount == 1) {
114
+ // Get the base widget name (remove constructor suffix if any)
115
+ final baseWidgetName = typeName.split('.').first;
116
+ final paramType = paramName == 'slivers' ? 'sliver' : 'child';
117
+
118
+ violations.add(analyzer.createViolation(
119
+ filePath: filePath,
120
+ line: analyzer.getLine(lineInfo, node.offset),
121
+ column: analyzer.getColumn(lineInfo, node.offset),
122
+ message: '$baseWidgetName has only one $paramType. Consider using a single-child widget like Container, SizedBox, or Padding instead.',
123
+ ));
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ void _checkChildrenCountFromInvocation(MethodInvocation node, String widgetName) {
132
+ final arguments = node.argumentList.arguments;
133
+
134
+ for (final arg in arguments) {
135
+ if (arg is NamedExpression) {
136
+ final paramName = arg.name.label.name;
137
+
138
+ // Check for 'children' or 'slivers' parameter
139
+ if (paramName == 'children' || paramName == 'slivers') {
140
+ final expression = arg.expression;
141
+
142
+ // Check if it's a list literal
143
+ if (expression is ListLiteral) {
144
+ final elementCount = expression.elements.length;
145
+
146
+ if (elementCount == 1) {
147
+ final paramType = paramName == 'slivers' ? 'sliver' : 'child';
148
+
149
+ violations.add(analyzer.createViolation(
150
+ filePath: filePath,
151
+ line: analyzer.getLine(lineInfo, node.offset),
152
+ column: analyzer.getColumn(lineInfo, node.offset),
153
+ message: '$widgetName has only one $paramType. Consider using a single-child widget like Container, SizedBox, or Padding instead.',
154
+ ));
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+ }
@@ -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
+ /// D020: Limit If/Else Branches
10
+ /// Limit the number of if/else branches to improve readability and maintainability
11
+ class D020LimitIfElseBranchesAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'D020';
14
+
15
+ // Default maximum number of if/else branches
16
+ static const int _defaultMaxBranches = 3;
17
+
18
+ @override
19
+ List<Violation> analyze({
20
+ required CompilationUnit unit,
21
+ required String filePath,
22
+ required Rule rule,
23
+ required LineInfo lineInfo,
24
+ }) {
25
+ final violations = <Violation>[];
26
+
27
+ // Get maxBranches from rule config, default to 3
28
+ final maxBranches = (rule.config['maxBranches'] as int?) ?? _defaultMaxBranches;
29
+
30
+ final visitor = _D020Visitor(
31
+ filePath: filePath,
32
+ lineInfo: lineInfo,
33
+ violations: violations,
34
+ analyzer: this,
35
+ maxBranches: maxBranches,
36
+ );
37
+
38
+ unit.accept(visitor);
39
+
40
+ return violations;
41
+ }
42
+ }
43
+
44
+ class _D020Visitor extends RecursiveAstVisitor<void> {
45
+ final String filePath;
46
+ final LineInfo lineInfo;
47
+ final List<Violation> violations;
48
+ final D020LimitIfElseBranchesAnalyzer analyzer;
49
+ final int maxBranches;
50
+
51
+ // Track if statements that are part of else-if chains to avoid duplicate reporting
52
+ final Set<int> _processedIfStatements = {};
53
+
54
+ _D020Visitor({
55
+ required this.filePath,
56
+ required this.lineInfo,
57
+ required this.violations,
58
+ required this.analyzer,
59
+ required this.maxBranches,
60
+ });
61
+
62
+ @override
63
+ void visitIfStatement(IfStatement node) {
64
+ // Skip if this if statement is already processed as part of an else-if chain
65
+ if (_processedIfStatements.contains(node.offset)) {
66
+ super.visitIfStatement(node);
67
+ return;
68
+ }
69
+
70
+ // Mark this if and all its else-if statements as processed
71
+ _markChainAsProcessed(node);
72
+
73
+ final branchCount = _countBranches(node);
74
+
75
+ if (branchCount > maxBranches) {
76
+ violations.add(analyzer.createViolation(
77
+ filePath: filePath,
78
+ line: analyzer.getLine(lineInfo, node.offset),
79
+ column: analyzer.getColumn(lineInfo, node.offset),
80
+ message: 'This if/else chain has $branchCount branches, exceeding the limit of $maxBranches. Consider using switch statement, polymorphism, or a lookup table instead.',
81
+ ));
82
+ }
83
+
84
+ super.visitIfStatement(node);
85
+ }
86
+
87
+ /// Mark all if statements in this else-if chain as processed
88
+ void _markChainAsProcessed(IfStatement node) {
89
+ _processedIfStatements.add(node.offset);
90
+
91
+ Statement? current = node.elseStatement;
92
+ while (current != null) {
93
+ if (current is IfStatement) {
94
+ _processedIfStatements.add(current.offset);
95
+ current = current.elseStatement;
96
+ } else {
97
+ break;
98
+ }
99
+ }
100
+ }
101
+
102
+ /// Count the number of branches in an if/else chain
103
+ /// Example:
104
+ /// if (a) { } -> 1 branch
105
+ /// if (a) { } else { } -> 2 branches
106
+ /// if (a) { } else if (b) { } else { } -> 3 branches
107
+ int _countBranches(IfStatement node) {
108
+ int count = 1; // Count the initial if
109
+
110
+ Statement? current = node.elseStatement;
111
+ while (current != null) {
112
+ count++;
113
+
114
+ // Check if this is an else-if
115
+ if (current is IfStatement) {
116
+ current = current.elseStatement;
117
+ } else {
118
+ // This is a final else, stop counting
119
+ break;
120
+ }
121
+ }
122
+
123
+ return count;
124
+ }
125
+ }