@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.
- package/dart_analyzer/README.md +226 -0
- package/dart_analyzer/analysis_options.yaml +66 -0
- package/dart_analyzer/bin/sunlint-dart-macos +0 -0
- package/dart_analyzer/bin/sunlint_dart_analyzer.dart +124 -0
- package/dart_analyzer/lib/analyzer_service.dart +625 -0
- package/dart_analyzer/lib/json_rpc_server.dart +275 -0
- package/dart_analyzer/lib/models/rule.dart +67 -0
- package/dart_analyzer/lib/models/symbol_table.dart +607 -0
- package/dart_analyzer/lib/models/violation.dart +69 -0
- package/dart_analyzer/lib/rules/base_analyzer.dart +52 -0
- package/dart_analyzer/lib/rules/common/C002_no_duplicate_code.dart +344 -0
- package/dart_analyzer/lib/rules/common/C003_no_vague_abbreviations.dart +318 -0
- package/dart_analyzer/lib/rules/common/C006_function_naming.dart +219 -0
- package/dart_analyzer/lib/rules/common/C008_variable_declaration_locality.dart +205 -0
- package/dart_analyzer/lib/rules/common/C010_limit_block_nesting.dart +162 -0
- package/dart_analyzer/lib/rules/common/C012_command_query_separation.dart +214 -0
- package/dart_analyzer/lib/rules/common/C013_no_dead_code.dart +225 -0
- package/dart_analyzer/lib/rules/common/C014_dependency_injection.dart +249 -0
- package/dart_analyzer/lib/rules/common/C017_constructor_logic.dart +158 -0
- package/dart_analyzer/lib/rules/common/C018_no_throw_generic_error.dart +141 -0
- package/dart_analyzer/lib/rules/common/C019_log_level_usage.dart +165 -0
- package/dart_analyzer/lib/rules/common/C020_unused_imports.dart +128 -0
- package/dart_analyzer/lib/rules/common/C021_import_organization.dart +86 -0
- package/dart_analyzer/lib/rules/common/C023_no_duplicate_variable.dart +112 -0
- package/dart_analyzer/lib/rules/common/C024_no_scatter_hardcoded_constants.dart +79 -0
- package/dart_analyzer/lib/rules/common/C029_catch_block_logging.dart +81 -0
- package/dart_analyzer/lib/rules/common/C030_use_custom_error_classes.dart +77 -0
- package/dart_analyzer/lib/rules/common/C031_validation_separation.dart +90 -0
- package/dart_analyzer/lib/rules/common/C033_separate_service_repository.dart +80 -0
- package/dart_analyzer/lib/rules/common/C035_error_logging_context.dart +148 -0
- package/dart_analyzer/lib/rules/common/C040_centralized_validation.dart +84 -0
- package/dart_analyzer/lib/rules/common/C041_no_sensitive_hardcode.dart +103 -0
- package/dart_analyzer/lib/rules/common/C042_boolean_name_prefix.dart +105 -0
- package/dart_analyzer/lib/rules/common/C043_no_console_or_print.dart +101 -0
- package/dart_analyzer/lib/rules/common/C047_no_duplicate_retry_logic.dart +94 -0
- package/dart_analyzer/lib/rules/common/C048_no_bypass_architectural_layers.dart +132 -0
- package/dart_analyzer/lib/rules/common/C052_parsing_or_data_transformation.dart +95 -0
- package/dart_analyzer/lib/rules/common/C060_no_override_superclass.dart +81 -0
- package/dart_analyzer/lib/rules/common/C065_one_behavior_per_test.dart +83 -0
- package/dart_analyzer/lib/rules/common/C067_no_hardcoded_config.dart +89 -0
- package/dart_analyzer/lib/rules/common/C070_no_real_time_tests.dart +99 -0
- package/dart_analyzer/lib/rules/common/C072_single_test_behavior.dart +78 -0
- package/dart_analyzer/lib/rules/common/C073_validate_required_config_on_startup.dart +82 -0
- package/dart_analyzer/lib/rules/common/C075_explicit_return_types.dart +85 -0
- package/dart_analyzer/lib/rules/common/C076_explicit_function_types.dart +104 -0
- package/dart_analyzer/lib/rules/dart/D001_recommended_lint_rules.dart +309 -0
- package/dart_analyzer/lib/rules/dart/D002_dispose_resources.dart +338 -0
- package/dart_analyzer/lib/rules/dart/D003_prefer_widgets_over_methods.dart +273 -0
- package/dart_analyzer/lib/rules/dart/D004_avoid_shrinkwrap_listview.dart +154 -0
- package/dart_analyzer/lib/rules/dart/D005_limit_widget_nesting.dart +265 -0
- package/dart_analyzer/lib/rules/dart/D006_prefer_extracting_large_callbacks.dart +135 -0
- package/dart_analyzer/lib/rules/dart/D007_prefer_init_first_dispose_last.dart +150 -0
- package/dart_analyzer/lib/rules/dart/D008_avoid_long_functions.dart +394 -0
- package/dart_analyzer/lib/rules/dart/D009_limit_function_parameters.dart +179 -0
- package/dart_analyzer/lib/rules/dart/D010_limit_cyclomatic_complexity.dart +257 -0
- package/dart_analyzer/lib/rules/dart/D011_prefer_named_parameters.dart +152 -0
- package/dart_analyzer/lib/rules/dart/D012_prefer_named_boolean_parameters.dart +156 -0
- package/dart_analyzer/lib/rules/dart/D013_single_public_class.dart +246 -0
- package/dart_analyzer/lib/rules/dart/D014_unsafe_collection_access.dart +202 -0
- package/dart_analyzer/lib/rules/dart/D015_copywith_all_parameters.dart +125 -0
- package/dart_analyzer/lib/rules/dart/D016_project_should_have_tests.dart +134 -0
- package/dart_analyzer/lib/rules/dart/D017_pubspec_dependencies_review.dart +187 -0
- package/dart_analyzer/lib/rules/dart/D018_remove_commented_code.dart +196 -0
- package/dart_analyzer/lib/rules/dart/D019_avoid_single_child_multi_child_widget.dart +161 -0
- package/dart_analyzer/lib/rules/dart/D020_limit_if_else_branches.dart +125 -0
- package/dart_analyzer/lib/rules/dart/D021_avoid_negated_boolean_checks.dart +227 -0
- package/dart_analyzer/lib/rules/dart/D022_use_setstate_correctly.dart +269 -0
- package/dart_analyzer/lib/rules/dart/D023_avoid_unnecessary_method_overrides.dart +191 -0
- package/dart_analyzer/lib/rules/dart/D024_avoid_unnecessary_stateful_widget.dart +194 -0
- package/dart_analyzer/lib/rules/dart/D025_avoid_nested_conditional_expressions.dart +90 -0
- package/dart_analyzer/lib/rules/security/S001_backend_auth_communications.dart +155 -0
- package/dart_analyzer/lib/rules/security/S002_os_command_injection.dart +159 -0
- package/dart_analyzer/lib/rules/security/S003_open_redirect_protection.dart +208 -0
- package/dart_analyzer/lib/rules/security/S004_sensitive_data_logging.dart +391 -0
- package/dart_analyzer/lib/rules/security/S005_trusted_service_authorization.dart +182 -0
- package/dart_analyzer/lib/rules/security/S006_no_default_credentials.dart +208 -0
- package/dart_analyzer/lib/rules/security/S007_output_encoding.dart +224 -0
- package/dart_analyzer/lib/rules/security/S008_svg_content_sanitization.dart +211 -0
- package/dart_analyzer/lib/rules/security/S009_no_insecure_encryption.dart +160 -0
- package/dart_analyzer/lib/rules/security/S010_use_csprng.dart +184 -0
- package/dart_analyzer/lib/rules/security/S011_ech_tls_config.dart +175 -0
- package/dart_analyzer/lib/rules/security/S012_hardcoded_secrets.dart +255 -0
- package/dart_analyzer/lib/rules/security/S013_tls_enforcement.dart +148 -0
- package/dart_analyzer/lib/rules/security/S014_tls_version_enforcement.dart +117 -0
- package/dart_analyzer/lib/rules/security/S015_insecure_tls_certificate.dart +315 -0
- package/dart_analyzer/lib/rules/security/S016_no_sensitive_querystring.dart +244 -0
- package/dart_analyzer/lib/rules/security/S017_use_parameterized_queries.dart +191 -0
- package/dart_analyzer/lib/rules/security/S018_no_sensitive_browser_storage.dart +175 -0
- package/dart_analyzer/lib/rules/security/S019_smtp_injection_protection.dart +166 -0
- package/dart_analyzer/lib/rules/security/S020_no_eval_dynamic_code.dart +149 -0
- package/dart_analyzer/lib/rules/security/S021_referrer_policy.dart +146 -0
- package/dart_analyzer/lib/rules/security/S022_escape_output_context.dart +111 -0
- package/dart_analyzer/lib/rules/security/S023_no_json_injection.dart +550 -0
- package/dart_analyzer/lib/rules/security/S024_xpath_xxe_protection.dart +299 -0
- package/dart_analyzer/lib/rules/security/S025_server_side_validation.dart +140 -0
- package/dart_analyzer/lib/rules/security/S026_tls_all_connections.dart +196 -0
- package/dart_analyzer/lib/rules/security/S027_mtls_certificate_validation.dart +195 -0
- package/dart_analyzer/lib/rules/security/S028_file_upload_size_limits.dart +186 -0
- package/dart_analyzer/lib/rules/security/S029_csrf_protection.dart +171 -0
- package/dart_analyzer/lib/rules/security/S030_directory_browsing_protection.dart +144 -0
- package/dart_analyzer/lib/rules/security/S031_secure_session_cookies.dart +118 -0
- package/dart_analyzer/lib/rules/security/S032_httponly_session_cookies.dart +114 -0
- package/dart_analyzer/lib/rules/security/S033_samesite_session_cookies.dart +120 -0
- package/dart_analyzer/lib/rules/security/S034_host_prefix_session_cookies.dart +160 -0
- package/dart_analyzer/lib/rules/security/S035_separate_app_hostnames.dart +117 -0
- package/dart_analyzer/lib/rules/security/S036_lfi_rfi_protection.dart +188 -0
- package/dart_analyzer/lib/rules/security/S037_cache_headers.dart +113 -0
- package/dart_analyzer/lib/rules/security/S038_no_version_headers.dart +114 -0
- package/dart_analyzer/lib/rules/security/S039_tls_certificate_validation.dart +131 -0
- package/dart_analyzer/lib/rules/security/S040_session_fixation_protection.dart +155 -0
- package/dart_analyzer/lib/rules/security/S041_session_token_invalidation.dart +201 -0
- package/dart_analyzer/lib/rules/security/S042_require_re_authentication_for_long_lived.dart +158 -0
- package/dart_analyzer/lib/rules/security/S043_password_changes_invalidate_all_sessions.dart +88 -0
- package/dart_analyzer/lib/rules/security/S044_re_authentication_required.dart +119 -0
- package/dart_analyzer/lib/rules/security/S045_brute_force_protection.dart +253 -0
- package/dart_analyzer/lib/rules/security/S046_jwt_algorithm_allowlist.dart +113 -0
- package/dart_analyzer/lib/rules/security/S047_oauth_pkce_protection.dart +124 -0
- package/dart_analyzer/lib/rules/security/S048_oauth_redirect_uri_validation.dart +134 -0
- package/dart_analyzer/lib/rules/security/S049_short_validity_tokens.dart +145 -0
- package/dart_analyzer/lib/rules/security/S050_reference_tokens_entropy.dart +234 -0
- package/dart_analyzer/lib/rules/security/S051_password_length_policy.dart +171 -0
- package/dart_analyzer/lib/rules/security/S052_weak_otp_entropy.dart +107 -0
- package/dart_analyzer/lib/rules/security/S053_generic_error_messages.dart +159 -0
- package/dart_analyzer/lib/rules/security/S054_no_default_accounts.dart +141 -0
- package/dart_analyzer/lib/rules/security/S055_content_type_validation.dart +324 -0
- package/dart_analyzer/lib/rules/security/S056_log_injection_protection.dart +119 -0
- package/dart_analyzer/lib/rules/security/S057_utc_logging.dart +114 -0
- package/dart_analyzer/lib/rules/security/S058_no_ssrf.dart +175 -0
- package/dart_analyzer/lib/rules/security/S059_disable_debug_mode.dart +172 -0
- package/dart_analyzer/lib/rules/security/S060_password_minimum_length.dart +170 -0
- package/dart_analyzer/lib/symbol_table_extractor.dart +510 -0
- package/dart_analyzer/lib/utils/common_utils.dart +26 -0
- package/dart_analyzer/pubspec.lock +557 -0
- package/dart_analyzer/pubspec.yaml +39 -0
- package/dart_analyzer/test/fixtures/complex_code.dart +95 -0
- package/docs/GENERATED_FILE_HANDLING_SUMMARY.md +2 -2
- 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
|
+
}
|