@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,52 @@
1
+ import 'package:analyzer/dart/ast/ast.dart';
2
+ import 'package:analyzer/source/line_info.dart';
3
+
4
+ import '../models/rule.dart';
5
+ import '../models/violation.dart';
6
+
7
+ /// Base class for all rule analyzers
8
+ abstract class BaseAnalyzer {
9
+ /// Rule ID this analyzer handles
10
+ String get ruleId;
11
+
12
+ /// Analyze a compilation unit and return violations
13
+ List<Violation> analyze({
14
+ required CompilationUnit unit,
15
+ required String filePath,
16
+ required Rule rule,
17
+ required LineInfo lineInfo,
18
+ });
19
+
20
+ /// Get line number from offset
21
+ int getLine(LineInfo lineInfo, int offset) {
22
+ return lineInfo.getLocation(offset).lineNumber;
23
+ }
24
+
25
+ /// Get column from offset
26
+ int getColumn(LineInfo lineInfo, int offset) {
27
+ return lineInfo.getLocation(offset).columnNumber;
28
+ }
29
+
30
+ /// Create a violation
31
+ Violation createViolation({
32
+ required String filePath,
33
+ required int line,
34
+ required int column,
35
+ required String message,
36
+ String severity = 'warning',
37
+ int? confidence,
38
+ Map<String, dynamic>? metadata,
39
+ }) {
40
+ return Violation(
41
+ ruleId: ruleId,
42
+ filePath: filePath,
43
+ line: line,
44
+ column: column,
45
+ message: message,
46
+ severity: severity,
47
+ analysisMethod: 'ast',
48
+ confidence: confidence,
49
+ metadata: metadata,
50
+ );
51
+ }
52
+ }
@@ -0,0 +1,344 @@
1
+ import 'dart:convert';
2
+ import 'package:analyzer/dart/ast/ast.dart';
3
+ import 'package:analyzer/dart/ast/visitor.dart';
4
+ import 'package:analyzer/source/line_info.dart';
5
+ import 'package:crypto/crypto.dart';
6
+
7
+ import '../../models/rule.dart';
8
+ import '../../models/violation.dart';
9
+ import '../base_analyzer.dart';
10
+
11
+ /// C002: No Duplicate Code (> 10 lines)
12
+ /// Detects duplicate code blocks that should be refactored
13
+ /// - Finds functions/methods with similar bodies
14
+ /// - Detects copy-pasted code blocks
15
+ /// - Suggests extracting common logic
16
+ class C002NoDuplicateCodeAnalyzer extends BaseAnalyzer {
17
+ @override
18
+ String get ruleId => 'C002';
19
+
20
+ /// Minimum lines for duplicate detection
21
+ static const int minDuplicateLines = 10;
22
+
23
+ /// Similarity threshold (0.0 - 1.0)
24
+ static const double similarityThreshold = 0.85;
25
+
26
+ /// Store code blocks for cross-file comparison
27
+ final Map<String, List<_CodeBlock>> _codeBlocks = {};
28
+
29
+ @override
30
+ List<Violation> analyze({
31
+ required CompilationUnit unit,
32
+ required String filePath,
33
+ required Rule rule,
34
+ required LineInfo lineInfo,
35
+ }) {
36
+ final violations = <Violation>[];
37
+
38
+ final visitor = _DuplicateCodeVisitor(
39
+ filePath: filePath,
40
+ lineInfo: lineInfo,
41
+ violations: violations,
42
+ analyzer: this,
43
+ codeBlocks: _codeBlocks,
44
+ );
45
+
46
+ unit.accept(visitor);
47
+
48
+ // Check for duplicates within collected blocks (only for current file)
49
+ _detectDuplicates(violations, filePath);
50
+
51
+ return violations;
52
+ }
53
+
54
+ void _detectDuplicates(List<Violation> violations, String currentFilePath) {
55
+ final allBlocks = _codeBlocks.values.expand((b) => b).toList();
56
+ final reportedPairs = <String>{};
57
+
58
+ // Performance: Get only blocks from current file to compare against others
59
+ final currentFileBlocks = _codeBlocks[currentFilePath] ?? [];
60
+ if (currentFileBlocks.isEmpty) return;
61
+
62
+ // Group blocks by hash for faster exact match lookups
63
+ final blocksByHash = <String, List<_CodeBlock>>{};
64
+ for (final block in allBlocks) {
65
+ blocksByHash.putIfAbsent(block.hash, () => []).add(block);
66
+ }
67
+
68
+ for (final currentBlock in currentFileBlocks) {
69
+ // Fast path: Check exact duplicates by hash first
70
+ final sameHashBlocks = blocksByHash[currentBlock.hash] ?? [];
71
+ for (final otherBlock in sameHashBlocks) {
72
+ // Skip self or same file overlapping blocks
73
+ if (otherBlock.filePath == currentFilePath) {
74
+ if (otherBlock == currentBlock || _overlaps(currentBlock, otherBlock)) {
75
+ continue;
76
+ }
77
+ }
78
+
79
+ final pairKey = _createPairKey(currentBlock, otherBlock);
80
+ if (reportedPairs.contains(pairKey)) continue;
81
+
82
+ reportedPairs.add(pairKey);
83
+ violations.add(createViolation(
84
+ filePath: currentBlock.filePath,
85
+ line: currentBlock.startLine,
86
+ column: 1,
87
+ message:
88
+ 'Duplicate code block found (${currentBlock.lineCount} lines) - same as ${otherBlock.filePath}:${otherBlock.startLine}',
89
+ severity: 'warning',
90
+ confidence: 100,
91
+ metadata: {
92
+ 'duplicateOf': '${otherBlock.filePath}:${otherBlock.startLine}',
93
+ 'lineCount': currentBlock.lineCount,
94
+ 'type': currentBlock.type,
95
+ 'similarity': 1.0,
96
+ },
97
+ ));
98
+ }
99
+
100
+ // Slow path: Check similar code (skip if already found exact match)
101
+ if (sameHashBlocks.length <= 1) {
102
+ for (final otherBlock in allBlocks) {
103
+ // Skip same file, self, or different file
104
+ if (otherBlock.filePath == currentFilePath) {
105
+ if (otherBlock == currentBlock || _overlaps(currentBlock, otherBlock)) {
106
+ continue;
107
+ }
108
+ }
109
+
110
+ // Skip if not enough lines
111
+ if (currentBlock.lineCount < minDuplicateLines ||
112
+ otherBlock.lineCount < minDuplicateLines) {
113
+ continue;
114
+ }
115
+
116
+ final pairKey = _createPairKey(currentBlock, otherBlock);
117
+ if (reportedPairs.contains(pairKey)) continue;
118
+
119
+ final similarity = _calculateSimilarity(currentBlock, otherBlock);
120
+ if (similarity >= similarityThreshold && similarity < 1.0) {
121
+ reportedPairs.add(pairKey);
122
+ violations.add(createViolation(
123
+ filePath: currentBlock.filePath,
124
+ line: currentBlock.startLine,
125
+ column: 1,
126
+ message:
127
+ 'Similar code block found (${(similarity * 100).toStringAsFixed(0)}% similar) - consider extracting common logic from ${otherBlock.filePath}:${otherBlock.startLine}',
128
+ severity: 'warning',
129
+ confidence: (similarity * 100).toInt(),
130
+ metadata: {
131
+ 'similarTo': '${otherBlock.filePath}:${otherBlock.startLine}',
132
+ 'lineCount': currentBlock.lineCount,
133
+ 'type': currentBlock.type,
134
+ 'similarity': similarity,
135
+ },
136
+ ));
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+
143
+ /// Create unique pair key to avoid duplicate reporting
144
+ String _createPairKey(_CodeBlock a, _CodeBlock b) {
145
+ final key1 = '${a.filePath}:${a.startLine}';
146
+ final key2 = '${b.filePath}:${b.startLine}';
147
+ // Sort keys to ensure consistent ordering
148
+ return key1.compareTo(key2) < 0 ? '$key1|$key2' : '$key2|$key1';
149
+ }
150
+
151
+ bool _overlaps(_CodeBlock a, _CodeBlock b) {
152
+ return !(a.endLine < b.startLine || b.endLine < a.startLine);
153
+ }
154
+
155
+ double _calculateSimilarity(_CodeBlock a, _CodeBlock b) {
156
+ final lines1 = a.normalizedLines;
157
+ final lines2 = b.normalizedLines;
158
+
159
+ if (lines1.isEmpty || lines2.isEmpty) return 0.0;
160
+
161
+ // Quick size difference check - if too different, skip expensive calculations
162
+ final maxLength = lines1.length > lines2.length ? lines1.length : lines2.length;
163
+ final minLength = lines1.length < lines2.length ? lines1.length : lines2.length;
164
+ final sizeDiff = (maxLength - minLength) / maxLength;
165
+ if (sizeDiff > 0.3) return 0.0; // More than 30% size difference
166
+
167
+ // Fast path: Line-by-line exact match similarity (cheapest)
168
+ var matchingLines = 0;
169
+ for (var i = 0; i < minLength; i++) {
170
+ if (lines1[i] == lines2[i]) {
171
+ matchingLines++;
172
+ }
173
+ }
174
+ final exactSimilarity = matchingLines / maxLength;
175
+
176
+ // Early return if exact similarity is already high enough
177
+ if (exactSimilarity >= similarityThreshold) {
178
+ return exactSimilarity;
179
+ }
180
+
181
+ // Medium cost: Token-based similarity (Jaccard)
182
+ final tokens1 = lines1.join(' ').split(RegExp(r'\s+'));
183
+ final tokens2 = lines2.join(' ').split(RegExp(r'\s+'));
184
+ final set1 = tokens1.toSet();
185
+ final set2 = tokens2.toSet();
186
+ final intersection = set1.intersection(set2).length;
187
+ final union = set1.union(set2).length;
188
+ final jaccardSimilarity = union > 0 ? intersection / union : 0.0;
189
+
190
+ // Early return if Jaccard is high enough
191
+ if (jaccardSimilarity >= similarityThreshold) {
192
+ return jaccardSimilarity;
193
+ }
194
+
195
+ // Most expensive: LCS only if needed
196
+ final lcsLength = _lcsLength(lines1, lines2);
197
+ final lcsSimilarity = lcsLength / maxLength;
198
+
199
+ // Return the maximum similarity
200
+ return [exactSimilarity, jaccardSimilarity, lcsSimilarity]
201
+ .reduce((a, b) => a > b ? a : b);
202
+ }
203
+
204
+ int _lcsLength(List<String> a, List<String> b) {
205
+ final m = a.length;
206
+ final n = b.length;
207
+ final dp = List.generate(m + 1, (_) => List.filled(n + 1, 0));
208
+
209
+ for (var i = 1; i <= m; i++) {
210
+ for (var j = 1; j <= n; j++) {
211
+ if (a[i - 1] == b[j - 1]) {
212
+ dp[i][j] = dp[i - 1][j - 1] + 1;
213
+ } else {
214
+ dp[i][j] = dp[i - 1][j] > dp[i][j - 1] ? dp[i - 1][j] : dp[i][j - 1];
215
+ }
216
+ }
217
+ }
218
+
219
+ return dp[m][n];
220
+ }
221
+
222
+ /// Clear collected blocks (call between analysis sessions)
223
+ void reset() {
224
+ _codeBlocks.clear();
225
+ }
226
+ }
227
+
228
+ class _CodeBlock {
229
+ final String filePath;
230
+ final int startLine;
231
+ final int endLine;
232
+ final String hash;
233
+ final List<String> normalizedLines;
234
+ final String type;
235
+
236
+ _CodeBlock({
237
+ required this.filePath,
238
+ required this.startLine,
239
+ required this.endLine,
240
+ required this.hash,
241
+ required this.normalizedLines,
242
+ required this.type,
243
+ });
244
+
245
+ int get lineCount => endLine - startLine + 1;
246
+ }
247
+
248
+ class _DuplicateCodeVisitor extends RecursiveAstVisitor<void> {
249
+ final String filePath;
250
+ final LineInfo lineInfo;
251
+ final List<Violation> violations;
252
+ final C002NoDuplicateCodeAnalyzer analyzer;
253
+ final Map<String, List<_CodeBlock>> codeBlocks;
254
+
255
+ _DuplicateCodeVisitor({
256
+ required this.filePath,
257
+ required this.lineInfo,
258
+ required this.violations,
259
+ required this.analyzer,
260
+ required this.codeBlocks,
261
+ });
262
+
263
+ @override
264
+ void visitFunctionDeclaration(FunctionDeclaration node) {
265
+ final body = node.functionExpression.body;
266
+ if (body is BlockFunctionBody) {
267
+ _processBlock(body.block, 'function', node.name.lexeme);
268
+ }
269
+ super.visitFunctionDeclaration(node);
270
+ }
271
+
272
+ @override
273
+ void visitMethodDeclaration(MethodDeclaration node) {
274
+ final body = node.body;
275
+ if (body is BlockFunctionBody) {
276
+ _processBlock(body.block, 'method', node.name.lexeme);
277
+ }
278
+ super.visitMethodDeclaration(node);
279
+ }
280
+
281
+ void _processBlock(Block block, String type, String name) {
282
+ final startLine = analyzer.getLine(lineInfo, block.offset);
283
+ final endLine = analyzer.getLine(lineInfo, block.end);
284
+ final lineCount = endLine - startLine + 1;
285
+
286
+ if (lineCount < C002NoDuplicateCodeAnalyzer.minDuplicateLines) return;
287
+
288
+ // Normalize code for comparison
289
+ final normalizedLines = _normalizeBlock(block);
290
+ final hash = _hashLines(normalizedLines);
291
+
292
+ final codeBlock = _CodeBlock(
293
+ filePath: filePath,
294
+ startLine: startLine,
295
+ endLine: endLine,
296
+ hash: hash,
297
+ normalizedLines: normalizedLines,
298
+ type: '$type:$name',
299
+ );
300
+
301
+ codeBlocks.putIfAbsent(filePath, () => []).add(codeBlock);
302
+ }
303
+
304
+ List<String> _normalizeBlock(Block block) {
305
+ final source = block.toSource();
306
+ return source
307
+ .split('\n')
308
+ .map((line) {
309
+ // Trim whitespace
310
+ var normalized = line.trim();
311
+
312
+ // Skip empty lines and comments
313
+ if (normalized.isEmpty ||
314
+ normalized.startsWith('//') ||
315
+ normalized.startsWith('/*')) {
316
+ return '';
317
+ }
318
+
319
+ // Normalize string literals (preserve quotes for structure)
320
+ normalized = normalized
321
+ .replaceAll(RegExp(r"'[^']*'"), "'_STR_'")
322
+ .replaceAll(RegExp(r'"[^"]*"'), '"_STR_"');
323
+
324
+ // Normalize numbers
325
+ normalized = normalized.replaceAll(RegExp(r'\b\d+\.?\d*\b'), '_NUM_');
326
+
327
+ // Normalize variable/parameter names (but keep method calls)
328
+ // Replace local variable patterns but preserve structure
329
+ normalized = normalized
330
+ .replaceAll(RegExp(r'\bfinal\s+\w+\s+='), 'final _var_ =')
331
+ .replaceAll(RegExp(r'\bvar\s+\w+\s+='), 'var _var_ =')
332
+ .replaceAll(RegExp(r'\b(\w+)\s*=\s*'), '_var_ = ');
333
+
334
+ return normalized;
335
+ })
336
+ .where((line) => line.isNotEmpty)
337
+ .toList();
338
+ }
339
+
340
+ String _hashLines(List<String> lines) {
341
+ final content = lines.join('\n');
342
+ return md5.convert(utf8.encode(content)).toString();
343
+ }
344
+ }
@@ -0,0 +1,318 @@
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
+ /// C003: No Vague Abbreviations
10
+ /// Ensures clear, understandable variable names without arbitrary abbreviations
11
+ /// - Detects unclear abbreviations (e.g., tmp, val, obj)
12
+ /// - Allows common well-known abbreviations (id, url, api)
13
+ /// - Enforces minimum name length
14
+ class C003NoVagueAbbreviationsAnalyzer extends BaseAnalyzer {
15
+ @override
16
+ String get ruleId => 'C003';
17
+
18
+ /// Minimum variable name length
19
+ static const int minNameLength = 2;
20
+
21
+ /// Allowed single character names (loop variables, etc.)
22
+ static final Set<String> allowedSingleChar = {
23
+ 'i', 'j', 'k', // Loop indices
24
+ 'x', 'y', 'z', // Coordinates
25
+ 'e', // Exception
26
+ '_', // Unused
27
+ 'a', 'b', // Generic in math
28
+ 'n', 'm', // Count/size
29
+ 't', // Time/temp
30
+ };
31
+
32
+ /// Well-known abbreviations that are allowed
33
+ static final Set<String> allowedAbbreviations = {
34
+ 'id',
35
+ 'Id',
36
+ 'ID',
37
+ 'url',
38
+ 'Url',
39
+ 'URL',
40
+ 'api',
41
+ 'Api',
42
+ 'API',
43
+ 'io',
44
+ 'IO',
45
+ 'db',
46
+ 'DB',
47
+ 'ui',
48
+ 'UI',
49
+ 'ok',
50
+ 'OK',
51
+ 'http',
52
+ 'HTTP',
53
+ 'json',
54
+ 'JSON',
55
+ 'xml',
56
+ 'XML',
57
+ 'html',
58
+ 'HTML',
59
+ 'css',
60
+ 'CSS',
61
+ 'sql',
62
+ 'SQL',
63
+ 'tcp',
64
+ 'TCP',
65
+ 'udp',
66
+ 'UDP',
67
+ 'ip',
68
+ 'IP',
69
+ 'os',
70
+ 'OS',
71
+ 'gc',
72
+ 'GC',
73
+ 'vm',
74
+ 'VM',
75
+ 'min',
76
+ 'max',
77
+ 'src',
78
+ 'dst',
79
+ 'err',
80
+ 'msg',
81
+ 'req',
82
+ 'res',
83
+ 'ctx',
84
+ 'cfg',
85
+ 'env',
86
+ 'idx',
87
+ 'len',
88
+ 'num',
89
+ 'str',
90
+ 'int',
91
+ 'ptr',
92
+ 'ref',
93
+ 'btn',
94
+ 'img',
95
+ 'doc',
96
+ 'dir',
97
+ 'tmp', // Allow tmp in some contexts
98
+ };
99
+
100
+ /// Vague abbreviations that should be avoided
101
+ static final Set<String> vagueAbbreviations = {
102
+ 'val',
103
+ 'obj',
104
+ 'data',
105
+ 'info',
106
+ 'item',
107
+ 'thing',
108
+ 'stuff',
109
+ 'foo',
110
+ 'bar',
111
+ 'baz',
112
+ 'qux',
113
+ 'temp',
114
+ 'tmp2',
115
+ 'var1',
116
+ 'var2',
117
+ 'str1',
118
+ 'str2',
119
+ 'num1',
120
+ 'num2',
121
+ 'test1',
122
+ 'test2',
123
+ 'xyz',
124
+ 'abc',
125
+ 'aaa',
126
+ 'bbb',
127
+ 'ccc',
128
+ 'xxx',
129
+ 'yyy',
130
+ 'zzz',
131
+ };
132
+
133
+ @override
134
+ List<Violation> analyze({
135
+ required CompilationUnit unit,
136
+ required String filePath,
137
+ required Rule rule,
138
+ required LineInfo lineInfo,
139
+ }) {
140
+ final violations = <Violation>[];
141
+
142
+ final visitor = _AbbreviationVisitor(
143
+ filePath: filePath,
144
+ lineInfo: lineInfo,
145
+ violations: violations,
146
+ analyzer: this,
147
+ );
148
+
149
+ unit.accept(visitor);
150
+
151
+ return violations;
152
+ }
153
+
154
+ /// Check if a name is a vague abbreviation
155
+ static bool isVagueAbbreviation(String name) {
156
+ final lowerName = name.toLowerCase().replaceAll('_', '');
157
+ return vagueAbbreviations.contains(lowerName);
158
+ }
159
+
160
+ /// Check if name is allowed despite being short
161
+ static bool isAllowedShortName(String name) {
162
+ // Remove leading underscores for private members
163
+ final cleanName = name.replaceAll(RegExp(r'^_+'), '');
164
+
165
+ if (cleanName.length == 1) {
166
+ return allowedSingleChar.contains(cleanName.toLowerCase());
167
+ }
168
+
169
+ return allowedAbbreviations.contains(cleanName) ||
170
+ allowedAbbreviations.contains(cleanName.toLowerCase());
171
+ }
172
+
173
+ /// Check if name looks like meaningless pattern (x1, y2, temp1, etc.)
174
+ static bool isMeaninglessPattern(String name) {
175
+ final cleanName = name.replaceAll(RegExp(r'^_+'), '');
176
+
177
+ // Single letter followed by numbers
178
+ if (RegExp(r'^[a-zA-Z]\d+$').hasMatch(cleanName)) {
179
+ return true;
180
+ }
181
+
182
+ // Common meaningless patterns
183
+ if (RegExp(r'^(temp|tmp|var|val|obj|data|item)\d*$', caseSensitive: false)
184
+ .hasMatch(cleanName)) {
185
+ return true;
186
+ }
187
+
188
+ return false;
189
+ }
190
+ }
191
+
192
+ class _AbbreviationVisitor extends RecursiveAstVisitor<void> {
193
+ final String filePath;
194
+ final LineInfo lineInfo;
195
+ final List<Violation> violations;
196
+ final C003NoVagueAbbreviationsAnalyzer analyzer;
197
+
198
+ _AbbreviationVisitor({
199
+ required this.filePath,
200
+ required this.lineInfo,
201
+ required this.violations,
202
+ required this.analyzer,
203
+ });
204
+
205
+ @override
206
+ void visitVariableDeclaration(VariableDeclaration node) {
207
+ _checkName(node.name.lexeme, node.name.offset, 'Variable');
208
+ super.visitVariableDeclaration(node);
209
+ }
210
+
211
+ @override
212
+ void visitFunctionDeclaration(FunctionDeclaration node) {
213
+ _checkName(node.name.lexeme, node.name.offset, 'Function');
214
+ super.visitFunctionDeclaration(node);
215
+ }
216
+
217
+ @override
218
+ void visitMethodDeclaration(MethodDeclaration node) {
219
+ // Skip operators
220
+ if (node.isOperator) return;
221
+ _checkName(node.name.lexeme, node.name.offset, 'Method');
222
+ super.visitMethodDeclaration(node);
223
+ }
224
+
225
+ @override
226
+ void visitSimpleFormalParameter(SimpleFormalParameter node) {
227
+ final name = node.name?.lexeme;
228
+ if (name != null) {
229
+ _checkName(name, node.name!.offset, 'Parameter');
230
+ }
231
+ super.visitSimpleFormalParameter(node);
232
+ }
233
+
234
+ @override
235
+ void visitFieldFormalParameter(FieldFormalParameter node) {
236
+ _checkName(node.name.lexeme, node.name.offset, 'Parameter');
237
+ super.visitFieldFormalParameter(node);
238
+ }
239
+
240
+ void _checkName(String name, int offset, String type) {
241
+ // Skip private prefix for checking
242
+ final cleanName = name.replaceAll(RegExp(r'^_+'), '');
243
+
244
+ // Skip if empty after cleaning
245
+ if (cleanName.isEmpty) return;
246
+
247
+ // Check for single character (unless allowed)
248
+ if (cleanName.length == 1 &&
249
+ !C003NoVagueAbbreviationsAnalyzer.allowedSingleChar
250
+ .contains(cleanName.toLowerCase())) {
251
+ violations.add(analyzer.createViolation(
252
+ filePath: filePath,
253
+ line: analyzer.getLine(lineInfo, offset),
254
+ column: analyzer.getColumn(lineInfo, offset),
255
+ message:
256
+ '$type name "$name" is too short - use a descriptive name',
257
+ metadata: {
258
+ 'name': name,
259
+ 'type': type.toLowerCase(),
260
+ 'issue': 'too_short',
261
+ },
262
+ ));
263
+ return;
264
+ }
265
+
266
+ // Check for vague abbreviations
267
+ if (C003NoVagueAbbreviationsAnalyzer.isVagueAbbreviation(cleanName)) {
268
+ violations.add(analyzer.createViolation(
269
+ filePath: filePath,
270
+ line: analyzer.getLine(lineInfo, offset),
271
+ column: analyzer.getColumn(lineInfo, offset),
272
+ message:
273
+ '$type name "$name" is too vague - use a more descriptive name',
274
+ metadata: {
275
+ 'name': name,
276
+ 'type': type.toLowerCase(),
277
+ 'issue': 'vague_abbreviation',
278
+ },
279
+ ));
280
+ return;
281
+ }
282
+
283
+ // Check for meaningless patterns
284
+ if (C003NoVagueAbbreviationsAnalyzer.isMeaninglessPattern(cleanName)) {
285
+ violations.add(analyzer.createViolation(
286
+ filePath: filePath,
287
+ line: analyzer.getLine(lineInfo, offset),
288
+ column: analyzer.getColumn(lineInfo, offset),
289
+ message:
290
+ '$type name "$name" follows a meaningless pattern - use a descriptive name',
291
+ metadata: {
292
+ 'name': name,
293
+ 'type': type.toLowerCase(),
294
+ 'issue': 'meaningless_pattern',
295
+ },
296
+ ));
297
+ return;
298
+ }
299
+
300
+ // Check minimum length for non-allowed abbreviations
301
+ if (cleanName.length < C003NoVagueAbbreviationsAnalyzer.minNameLength &&
302
+ !C003NoVagueAbbreviationsAnalyzer.isAllowedShortName(cleanName)) {
303
+ violations.add(analyzer.createViolation(
304
+ filePath: filePath,
305
+ line: analyzer.getLine(lineInfo, offset),
306
+ column: analyzer.getColumn(lineInfo, offset),
307
+ message:
308
+ '$type name "$name" is too short (min ${C003NoVagueAbbreviationsAnalyzer.minNameLength} chars) - use a descriptive name',
309
+ metadata: {
310
+ 'name': name,
311
+ 'type': type.toLowerCase(),
312
+ 'issue': 'too_short',
313
+ 'minLength': C003NoVagueAbbreviationsAnalyzer.minNameLength,
314
+ },
315
+ ));
316
+ }
317
+ }
318
+ }