@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,158 @@
|
|
|
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
|
+
/// C017: Constructor Logic
|
|
10
|
+
/// Do not put business logic inside constructors
|
|
11
|
+
class C017ConstructorLogicAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'C017';
|
|
14
|
+
|
|
15
|
+
/// Maximum allowed statements in constructor
|
|
16
|
+
static const int maxConstructorStatements = 5;
|
|
17
|
+
|
|
18
|
+
/// Patterns that indicate business logic
|
|
19
|
+
static final Set<String> businessLogicPatterns = {
|
|
20
|
+
'fetch',
|
|
21
|
+
'load',
|
|
22
|
+
'save',
|
|
23
|
+
'delete',
|
|
24
|
+
'update',
|
|
25
|
+
'create',
|
|
26
|
+
'process',
|
|
27
|
+
'calculate',
|
|
28
|
+
'validate',
|
|
29
|
+
'send',
|
|
30
|
+
'notify',
|
|
31
|
+
'http',
|
|
32
|
+
'api',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
@override
|
|
36
|
+
List<Violation> analyze({
|
|
37
|
+
required CompilationUnit unit,
|
|
38
|
+
required String filePath,
|
|
39
|
+
required Rule rule,
|
|
40
|
+
required LineInfo lineInfo,
|
|
41
|
+
}) {
|
|
42
|
+
final violations = <Violation>[];
|
|
43
|
+
|
|
44
|
+
final visitor = _ConstructorLogicVisitor(
|
|
45
|
+
filePath: filePath,
|
|
46
|
+
lineInfo: lineInfo,
|
|
47
|
+
violations: violations,
|
|
48
|
+
analyzer: this,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
unit.accept(visitor);
|
|
52
|
+
|
|
53
|
+
return violations;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class _ConstructorLogicVisitor extends RecursiveAstVisitor<void> {
|
|
58
|
+
final String filePath;
|
|
59
|
+
final LineInfo lineInfo;
|
|
60
|
+
final List<Violation> violations;
|
|
61
|
+
final C017ConstructorLogicAnalyzer analyzer;
|
|
62
|
+
|
|
63
|
+
_ConstructorLogicVisitor({
|
|
64
|
+
required this.filePath,
|
|
65
|
+
required this.lineInfo,
|
|
66
|
+
required this.violations,
|
|
67
|
+
required this.analyzer,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
@override
|
|
71
|
+
void visitConstructorDeclaration(ConstructorDeclaration node) {
|
|
72
|
+
if (node.body is BlockFunctionBody) {
|
|
73
|
+
final body = node.body as BlockFunctionBody;
|
|
74
|
+
final statements = body.block.statements;
|
|
75
|
+
|
|
76
|
+
// Check for too many statements
|
|
77
|
+
if (statements.length > C017ConstructorLogicAnalyzer.maxConstructorStatements) {
|
|
78
|
+
violations.add(analyzer.createViolation(
|
|
79
|
+
filePath: filePath,
|
|
80
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
81
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
82
|
+
message:
|
|
83
|
+
'Constructor has ${statements.length} statements (max: ${C017ConstructorLogicAnalyzer.maxConstructorStatements}) - consider using init method',
|
|
84
|
+
metadata: {
|
|
85
|
+
'statementCount': statements.length,
|
|
86
|
+
'maxStatements': C017ConstructorLogicAnalyzer.maxConstructorStatements,
|
|
87
|
+
'issue': 'too_many_statements',
|
|
88
|
+
},
|
|
89
|
+
));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check for business logic calls
|
|
93
|
+
final logicFinder = _BusinessLogicFinder(
|
|
94
|
+
filePath: filePath,
|
|
95
|
+
lineInfo: lineInfo,
|
|
96
|
+
violations: violations,
|
|
97
|
+
analyzer: analyzer,
|
|
98
|
+
);
|
|
99
|
+
body.block.accept(logicFinder);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
super.visitConstructorDeclaration(node);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class _BusinessLogicFinder extends RecursiveAstVisitor<void> {
|
|
107
|
+
final String filePath;
|
|
108
|
+
final LineInfo lineInfo;
|
|
109
|
+
final List<Violation> violations;
|
|
110
|
+
final C017ConstructorLogicAnalyzer analyzer;
|
|
111
|
+
|
|
112
|
+
_BusinessLogicFinder({
|
|
113
|
+
required this.filePath,
|
|
114
|
+
required this.lineInfo,
|
|
115
|
+
required this.violations,
|
|
116
|
+
required this.analyzer,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
@override
|
|
120
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
121
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
122
|
+
|
|
123
|
+
for (final pattern in C017ConstructorLogicAnalyzer.businessLogicPatterns) {
|
|
124
|
+
if (methodName.contains(pattern)) {
|
|
125
|
+
violations.add(analyzer.createViolation(
|
|
126
|
+
filePath: filePath,
|
|
127
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
128
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
129
|
+
message:
|
|
130
|
+
'Business logic method "${node.methodName.name}" should not be called in constructor',
|
|
131
|
+
metadata: {
|
|
132
|
+
'method': node.methodName.name,
|
|
133
|
+
'pattern': pattern,
|
|
134
|
+
'issue': 'business_logic_in_constructor',
|
|
135
|
+
},
|
|
136
|
+
));
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
super.visitMethodInvocation(node);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@override
|
|
145
|
+
void visitAwaitExpression(AwaitExpression node) {
|
|
146
|
+
violations.add(analyzer.createViolation(
|
|
147
|
+
filePath: filePath,
|
|
148
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
149
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
150
|
+
message: 'Await in constructor indicates async business logic - consider using factory or init method',
|
|
151
|
+
metadata: {
|
|
152
|
+
'issue': 'await_in_constructor',
|
|
153
|
+
},
|
|
154
|
+
));
|
|
155
|
+
|
|
156
|
+
super.visitAwaitExpression(node);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
/// C018: No Throw Generic Error
|
|
10
|
+
/// Always provide detailed messages and context for errors
|
|
11
|
+
class C018NoThrowGenericErrorAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'C018';
|
|
14
|
+
|
|
15
|
+
/// Generic exception types that should be avoided
|
|
16
|
+
static final Set<String> genericExceptions = {
|
|
17
|
+
'Exception',
|
|
18
|
+
'Error',
|
|
19
|
+
'StateError',
|
|
20
|
+
'ArgumentError',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/// Minimum message length for meaningful error messages
|
|
24
|
+
static const int minMessageLength = 10;
|
|
25
|
+
|
|
26
|
+
@override
|
|
27
|
+
List<Violation> analyze({
|
|
28
|
+
required CompilationUnit unit,
|
|
29
|
+
required String filePath,
|
|
30
|
+
required Rule rule,
|
|
31
|
+
required LineInfo lineInfo,
|
|
32
|
+
}) {
|
|
33
|
+
final violations = <Violation>[];
|
|
34
|
+
|
|
35
|
+
final visitor = _GenericErrorVisitor(
|
|
36
|
+
filePath: filePath,
|
|
37
|
+
lineInfo: lineInfo,
|
|
38
|
+
violations: violations,
|
|
39
|
+
analyzer: this,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
unit.accept(visitor);
|
|
43
|
+
|
|
44
|
+
return violations;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class _GenericErrorVisitor extends RecursiveAstVisitor<void> {
|
|
49
|
+
final String filePath;
|
|
50
|
+
final LineInfo lineInfo;
|
|
51
|
+
final List<Violation> violations;
|
|
52
|
+
final C018NoThrowGenericErrorAnalyzer analyzer;
|
|
53
|
+
|
|
54
|
+
_GenericErrorVisitor({
|
|
55
|
+
required this.filePath,
|
|
56
|
+
required this.lineInfo,
|
|
57
|
+
required this.violations,
|
|
58
|
+
required this.analyzer,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
@override
|
|
62
|
+
void visitThrowExpression(ThrowExpression node) {
|
|
63
|
+
final expression = node.expression;
|
|
64
|
+
|
|
65
|
+
String? typeName;
|
|
66
|
+
NodeList<Expression>? args;
|
|
67
|
+
|
|
68
|
+
// Handle both InstanceCreationExpression and MethodInvocation
|
|
69
|
+
// In Dart, `throw Exception('msg')` can be parsed as MethodInvocation
|
|
70
|
+
// when not using 'new' keyword
|
|
71
|
+
if (expression is InstanceCreationExpression) {
|
|
72
|
+
typeName = expression.constructorName.type.name2.lexeme;
|
|
73
|
+
args = expression.argumentList.arguments;
|
|
74
|
+
} else if (expression is MethodInvocation) {
|
|
75
|
+
// For calls like Exception('message') or Error()
|
|
76
|
+
typeName = expression.methodName.name;
|
|
77
|
+
args = expression.argumentList.arguments;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (typeName == null || args == null) {
|
|
81
|
+
super.visitThrowExpression(node);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check if it's a generic exception
|
|
86
|
+
if (C018NoThrowGenericErrorAnalyzer.genericExceptions.contains(typeName)) {
|
|
87
|
+
// Check if message is provided and meaningful
|
|
88
|
+
if (args.isEmpty) {
|
|
89
|
+
violations.add(analyzer.createViolation(
|
|
90
|
+
filePath: filePath,
|
|
91
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
92
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
93
|
+
message: 'throw $typeName() without message - provide a descriptive error message',
|
|
94
|
+
metadata: {
|
|
95
|
+
'exceptionType': typeName,
|
|
96
|
+
'issue': 'no_message',
|
|
97
|
+
},
|
|
98
|
+
));
|
|
99
|
+
} else {
|
|
100
|
+
final firstArg = args.first;
|
|
101
|
+
if (firstArg is StringLiteral) {
|
|
102
|
+
final message = firstArg.stringValue ?? '';
|
|
103
|
+
if (message.length < C018NoThrowGenericErrorAnalyzer.minMessageLength) {
|
|
104
|
+
violations.add(analyzer.createViolation(
|
|
105
|
+
filePath: filePath,
|
|
106
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
107
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
108
|
+
message:
|
|
109
|
+
'throw $typeName with short message "$message" - provide more context',
|
|
110
|
+
metadata: {
|
|
111
|
+
'exceptionType': typeName,
|
|
112
|
+
'message': message,
|
|
113
|
+
'issue': 'short_message',
|
|
114
|
+
},
|
|
115
|
+
));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Suggest using custom exception
|
|
121
|
+
if (typeName == 'Exception' || typeName == 'Error') {
|
|
122
|
+
final isException = typeName == 'Exception';
|
|
123
|
+
violations.add(analyzer.createViolation(
|
|
124
|
+
filePath: filePath,
|
|
125
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
126
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
127
|
+
message:
|
|
128
|
+
'Avoid throwing generic ${isException ? 'Exception' : 'Error'} - use a specific ${isException ? 'Exception' : 'Error'} type',
|
|
129
|
+
metadata: {
|
|
130
|
+
'exceptionType': typeName,
|
|
131
|
+
'issue': 'generic_exception',
|
|
132
|
+
'suggestion':
|
|
133
|
+
'Create a custom ${isException ? 'Exception' : 'Error'} class',
|
|
134
|
+
},
|
|
135
|
+
));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
super.visitThrowExpression(node);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
/// C019: Log Level Usage
|
|
10
|
+
/// Do not use error log level for non-critical errors
|
|
11
|
+
class C019LogLevelUsageAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'C019';
|
|
14
|
+
|
|
15
|
+
/// Keywords that indicate non-critical issues
|
|
16
|
+
static final Set<String> nonCriticalKeywords = {
|
|
17
|
+
'not found',
|
|
18
|
+
'invalid',
|
|
19
|
+
'unauthorized',
|
|
20
|
+
'forbidden',
|
|
21
|
+
'validation failed',
|
|
22
|
+
'bad request',
|
|
23
|
+
'cache miss',
|
|
24
|
+
'retry',
|
|
25
|
+
'fallback',
|
|
26
|
+
'user error',
|
|
27
|
+
'input error',
|
|
28
|
+
'validation',
|
|
29
|
+
'missing parameter',
|
|
30
|
+
'exceed',
|
|
31
|
+
'limit',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/// Keywords that indicate legitimate errors
|
|
35
|
+
static final Set<String> legitimateErrorKeywords = {
|
|
36
|
+
'exception',
|
|
37
|
+
'crash',
|
|
38
|
+
'fatal',
|
|
39
|
+
'critical',
|
|
40
|
+
'emergency',
|
|
41
|
+
'database',
|
|
42
|
+
'connection',
|
|
43
|
+
'timeout',
|
|
44
|
+
'security breach',
|
|
45
|
+
'system error',
|
|
46
|
+
'memory',
|
|
47
|
+
'disk space',
|
|
48
|
+
'internal server error',
|
|
49
|
+
'unhandled exception',
|
|
50
|
+
'stack overflow',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/// Error logger patterns
|
|
54
|
+
static final RegExp errorLogPattern = RegExp(
|
|
55
|
+
r'\.(error|e)\s*\(',
|
|
56
|
+
caseSensitive: false,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
@override
|
|
60
|
+
List<Violation> analyze({
|
|
61
|
+
required CompilationUnit unit,
|
|
62
|
+
required String filePath,
|
|
63
|
+
required Rule rule,
|
|
64
|
+
required LineInfo lineInfo,
|
|
65
|
+
}) {
|
|
66
|
+
final violations = <Violation>[];
|
|
67
|
+
|
|
68
|
+
final visitor = _LogLevelVisitor(
|
|
69
|
+
filePath: filePath,
|
|
70
|
+
lineInfo: lineInfo,
|
|
71
|
+
violations: violations,
|
|
72
|
+
analyzer: this,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
unit.accept(visitor);
|
|
76
|
+
|
|
77
|
+
return violations;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
class _LogLevelVisitor extends RecursiveAstVisitor<void> {
|
|
82
|
+
final String filePath;
|
|
83
|
+
final LineInfo lineInfo;
|
|
84
|
+
final List<Violation> violations;
|
|
85
|
+
final C019LogLevelUsageAnalyzer analyzer;
|
|
86
|
+
|
|
87
|
+
_LogLevelVisitor({
|
|
88
|
+
required this.filePath,
|
|
89
|
+
required this.lineInfo,
|
|
90
|
+
required this.violations,
|
|
91
|
+
required this.analyzer,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
@override
|
|
95
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
96
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
97
|
+
|
|
98
|
+
// Check if it's an error log call
|
|
99
|
+
if (methodName == 'error' || methodName == 'e') {
|
|
100
|
+
final args = node.argumentList.arguments;
|
|
101
|
+
|
|
102
|
+
if (args.isNotEmpty) {
|
|
103
|
+
final firstArg = args.first;
|
|
104
|
+
String? messageContent;
|
|
105
|
+
|
|
106
|
+
if (firstArg is SimpleStringLiteral) {
|
|
107
|
+
// Simple string like 'hello world'
|
|
108
|
+
messageContent = firstArg.value.toLowerCase();
|
|
109
|
+
} else if (firstArg is StringInterpolation) {
|
|
110
|
+
// Interpolated string like 'hello $name'
|
|
111
|
+
// Extract text elements only
|
|
112
|
+
final buffer = StringBuffer();
|
|
113
|
+
for (final element in firstArg.elements) {
|
|
114
|
+
if (element is InterpolationString) {
|
|
115
|
+
buffer.write(element.value);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
messageContent = buffer.toString().toLowerCase();
|
|
119
|
+
} else if (firstArg is AdjacentStrings) {
|
|
120
|
+
// Adjacent strings like 'hello' 'world'
|
|
121
|
+
final buffer = StringBuffer();
|
|
122
|
+
for (final str in firstArg.strings) {
|
|
123
|
+
if (str is SimpleStringLiteral) {
|
|
124
|
+
buffer.write(str.value);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
messageContent = buffer.toString().toLowerCase();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (messageContent != null) {
|
|
131
|
+
// Check for non-critical keywords
|
|
132
|
+
for (final keyword in C019LogLevelUsageAnalyzer.nonCriticalKeywords) {
|
|
133
|
+
if (messageContent.contains(keyword)) {
|
|
134
|
+
// But skip if it also contains legitimate error keywords
|
|
135
|
+
bool hasLegitimateError = false;
|
|
136
|
+
for (final legit in C019LogLevelUsageAnalyzer.legitimateErrorKeywords) {
|
|
137
|
+
if (messageContent.contains(legit)) {
|
|
138
|
+
hasLegitimateError = true;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!hasLegitimateError) {
|
|
144
|
+
violations.add(analyzer.createViolation(
|
|
145
|
+
filePath: filePath,
|
|
146
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
147
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
148
|
+
message:
|
|
149
|
+
'log.error() used for non-critical issue "$keyword" - consider using log.warn()',
|
|
150
|
+
metadata: {
|
|
151
|
+
'keyword': keyword,
|
|
152
|
+
'suggestion': 'Use log.warn() or log.info() instead',
|
|
153
|
+
},
|
|
154
|
+
));
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
super.visitMethodInvocation(node);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
/// C020: Unused Imports
|
|
10
|
+
/// Do not import unused modules or symbols
|
|
11
|
+
class C020UnusedImportsAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'C020';
|
|
14
|
+
|
|
15
|
+
@override
|
|
16
|
+
List<Violation> analyze({
|
|
17
|
+
required CompilationUnit unit,
|
|
18
|
+
required String filePath,
|
|
19
|
+
required Rule rule,
|
|
20
|
+
required LineInfo lineInfo,
|
|
21
|
+
}) {
|
|
22
|
+
final violations = <Violation>[];
|
|
23
|
+
|
|
24
|
+
// Collect all imports
|
|
25
|
+
final imports = <ImportDirective>[];
|
|
26
|
+
final importedPrefixes = <String, ImportDirective>{};
|
|
27
|
+
final showClauses = <String, ImportDirective>{};
|
|
28
|
+
|
|
29
|
+
for (final directive in unit.directives) {
|
|
30
|
+
if (directive is ImportDirective) {
|
|
31
|
+
imports.add(directive);
|
|
32
|
+
|
|
33
|
+
// Track prefixed imports
|
|
34
|
+
final prefix = directive.prefix?.name;
|
|
35
|
+
if (prefix != null) {
|
|
36
|
+
importedPrefixes[prefix] = directive;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Track show clauses
|
|
40
|
+
for (final combinator in directive.combinators) {
|
|
41
|
+
if (combinator is ShowCombinator) {
|
|
42
|
+
for (final name in combinator.shownNames) {
|
|
43
|
+
showClauses[name.name] = directive;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Collect all identifiers used in the file
|
|
51
|
+
final usedIdentifiers = <String>{};
|
|
52
|
+
final usedPrefixes = <String>{};
|
|
53
|
+
|
|
54
|
+
final visitor = _IdentifierCollector(
|
|
55
|
+
usedIdentifiers: usedIdentifiers,
|
|
56
|
+
usedPrefixes: usedPrefixes,
|
|
57
|
+
);
|
|
58
|
+
unit.accept(visitor);
|
|
59
|
+
|
|
60
|
+
// Check for unused prefixed imports
|
|
61
|
+
for (final entry in importedPrefixes.entries) {
|
|
62
|
+
if (!usedPrefixes.contains(entry.key)) {
|
|
63
|
+
violations.add(createViolation(
|
|
64
|
+
filePath: filePath,
|
|
65
|
+
line: getLine(lineInfo, entry.value.offset),
|
|
66
|
+
column: getColumn(lineInfo, entry.value.offset),
|
|
67
|
+
message: 'Import with prefix "${entry.key}" is never used',
|
|
68
|
+
metadata: {
|
|
69
|
+
'prefix': entry.key,
|
|
70
|
+
'import': entry.value.uri.stringValue ?? '',
|
|
71
|
+
'issue': 'unused_prefix',
|
|
72
|
+
},
|
|
73
|
+
));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check for unused show clause symbols
|
|
78
|
+
for (final entry in showClauses.entries) {
|
|
79
|
+
if (!usedIdentifiers.contains(entry.key)) {
|
|
80
|
+
violations.add(createViolation(
|
|
81
|
+
filePath: filePath,
|
|
82
|
+
line: getLine(lineInfo, entry.value.offset),
|
|
83
|
+
column: getColumn(lineInfo, entry.value.offset),
|
|
84
|
+
message: 'Symbol "${entry.key}" from import is never used',
|
|
85
|
+
metadata: {
|
|
86
|
+
'symbol': entry.key,
|
|
87
|
+
'import': entry.value.uri.stringValue ?? '',
|
|
88
|
+
'issue': 'unused_symbol',
|
|
89
|
+
},
|
|
90
|
+
));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return violations;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class _IdentifierCollector extends RecursiveAstVisitor<void> {
|
|
99
|
+
final Set<String> usedIdentifiers;
|
|
100
|
+
final Set<String> usedPrefixes;
|
|
101
|
+
|
|
102
|
+
_IdentifierCollector({
|
|
103
|
+
required this.usedIdentifiers,
|
|
104
|
+
required this.usedPrefixes,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
@override
|
|
108
|
+
void visitSimpleIdentifier(SimpleIdentifier node) {
|
|
109
|
+
usedIdentifiers.add(node.name);
|
|
110
|
+
super.visitSimpleIdentifier(node);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@override
|
|
114
|
+
void visitPrefixedIdentifier(PrefixedIdentifier node) {
|
|
115
|
+
usedPrefixes.add(node.prefix.name);
|
|
116
|
+
usedIdentifiers.add(node.identifier.name);
|
|
117
|
+
super.visitPrefixedIdentifier(node);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@override
|
|
121
|
+
void visitNamedType(NamedType node) {
|
|
122
|
+
if (node.importPrefix != null) {
|
|
123
|
+
usedPrefixes.add(node.importPrefix!.name.lexeme);
|
|
124
|
+
}
|
|
125
|
+
usedIdentifiers.add(node.name2.lexeme);
|
|
126
|
+
super.visitNamedType(node);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
/// C021: Import Organization
|
|
10
|
+
/// Enforce organized imports with grouping and sorting
|
|
11
|
+
class C021ImportOrganizationAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'C021';
|
|
14
|
+
|
|
15
|
+
@override
|
|
16
|
+
List<Violation> analyze({
|
|
17
|
+
required CompilationUnit unit,
|
|
18
|
+
required String filePath,
|
|
19
|
+
required Rule rule,
|
|
20
|
+
required LineInfo lineInfo,
|
|
21
|
+
}) {
|
|
22
|
+
final violations = <Violation>[];
|
|
23
|
+
|
|
24
|
+
final visitor = _C021Visitor(
|
|
25
|
+
filePath: filePath,
|
|
26
|
+
lineInfo: lineInfo,
|
|
27
|
+
violations: violations,
|
|
28
|
+
analyzer: this,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
unit.accept(visitor);
|
|
32
|
+
|
|
33
|
+
return violations;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class _C021Visitor extends RecursiveAstVisitor<void> {
|
|
38
|
+
final String filePath;
|
|
39
|
+
final LineInfo lineInfo;
|
|
40
|
+
final List<Violation> violations;
|
|
41
|
+
final C021ImportOrganizationAnalyzer analyzer;
|
|
42
|
+
|
|
43
|
+
String _lastImportType = '';
|
|
44
|
+
|
|
45
|
+
_C021Visitor({
|
|
46
|
+
required this.filePath,
|
|
47
|
+
required this.lineInfo,
|
|
48
|
+
required this.violations,
|
|
49
|
+
required this.analyzer,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
@override
|
|
53
|
+
void visitImportDirective(ImportDirective node) {
|
|
54
|
+
// Check import organization
|
|
55
|
+
final uri = node.uri.stringValue ?? '';
|
|
56
|
+
|
|
57
|
+
// Track import order for validation
|
|
58
|
+
// Dart imports should follow: dart:, package:, relative
|
|
59
|
+
if (uri.startsWith('dart:')) {
|
|
60
|
+
if (_lastImportType == 'package' || _lastImportType == 'relative') {
|
|
61
|
+
violations.add(analyzer.createViolation(
|
|
62
|
+
filePath: filePath,
|
|
63
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
64
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
65
|
+
message: 'dart: imports should come before package: and relative imports',
|
|
66
|
+
));
|
|
67
|
+
}
|
|
68
|
+
_lastImportType = 'dart';
|
|
69
|
+
} else if (uri.startsWith('package:')) {
|
|
70
|
+
if (_lastImportType == 'relative') {
|
|
71
|
+
violations.add(analyzer.createViolation(
|
|
72
|
+
filePath: filePath,
|
|
73
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
74
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
75
|
+
message: 'package: imports should come before relative imports',
|
|
76
|
+
));
|
|
77
|
+
}
|
|
78
|
+
_lastImportType = 'package';
|
|
79
|
+
} else {
|
|
80
|
+
_lastImportType = 'relative';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
super.visitImportDirective(node);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
}
|