@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,394 @@
|
|
|
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
|
+
/// D008: Avoid Long Functions
|
|
10
|
+
/// Functions should not exceed the maximum line limit
|
|
11
|
+
class D008AvoidLongFunctionsAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'D008';
|
|
14
|
+
|
|
15
|
+
// Default maximum lines for a function
|
|
16
|
+
static const int _defaultMaxLines = 60;
|
|
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 maxLines from rule config, default to 60
|
|
28
|
+
final maxLines = (rule.config['maxLines'] as int?) ?? _defaultMaxLines;
|
|
29
|
+
|
|
30
|
+
final visitor = _D008Visitor(
|
|
31
|
+
filePath: filePath,
|
|
32
|
+
lineInfo: lineInfo,
|
|
33
|
+
violations: violations,
|
|
34
|
+
analyzer: this,
|
|
35
|
+
maxLines: maxLines,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
unit.accept(visitor);
|
|
39
|
+
|
|
40
|
+
return violations;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class _D008Visitor extends RecursiveAstVisitor<void> {
|
|
45
|
+
final String filePath;
|
|
46
|
+
final LineInfo lineInfo;
|
|
47
|
+
final List<Violation> violations;
|
|
48
|
+
final D008AvoidLongFunctionsAnalyzer analyzer;
|
|
49
|
+
final int maxLines;
|
|
50
|
+
|
|
51
|
+
_D008Visitor({
|
|
52
|
+
required this.filePath,
|
|
53
|
+
required this.lineInfo,
|
|
54
|
+
required this.violations,
|
|
55
|
+
required this.analyzer,
|
|
56
|
+
required this.maxLines,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
@override
|
|
60
|
+
void visitFunctionDeclaration(FunctionDeclaration node) {
|
|
61
|
+
final isBuildMethod = false; // Functions can't be build method
|
|
62
|
+
final isWidgetBuilder = _isWidgetBuilder(node.returnType, node.functionExpression.body);
|
|
63
|
+
|
|
64
|
+
if (isWidgetBuilder) {
|
|
65
|
+
final logicLines = _countLogicLines(node.functionExpression.body);
|
|
66
|
+
if (logicLines <= 10) {
|
|
67
|
+
super.visitFunctionDeclaration(node);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Widget builder with too much logic - treat as long function
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_checkFunctionLength(
|
|
74
|
+
node.functionExpression.body,
|
|
75
|
+
node.name.lexeme,
|
|
76
|
+
node.offset,
|
|
77
|
+
isBuildMethod: isBuildMethod,
|
|
78
|
+
isWidgetBuilder: isWidgetBuilder,
|
|
79
|
+
);
|
|
80
|
+
super.visitFunctionDeclaration(node);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@override
|
|
84
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
85
|
+
final isBuildMethod = node.name.lexeme == 'build';
|
|
86
|
+
final isWidgetBuilder = _isWidgetBuilder(node.returnType, node.body) || isBuildMethod;
|
|
87
|
+
|
|
88
|
+
if (isWidgetBuilder) {
|
|
89
|
+
final logicLines = _countLogicLines(node.body);
|
|
90
|
+
if (logicLines <= 10) {
|
|
91
|
+
super.visitMethodDeclaration(node);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// Widget builder or build with too much logic - continue to check
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_checkFunctionLength(
|
|
98
|
+
node.body,
|
|
99
|
+
node.name.lexeme,
|
|
100
|
+
node.offset,
|
|
101
|
+
isBuildMethod: isBuildMethod,
|
|
102
|
+
isWidgetBuilder: isWidgetBuilder,
|
|
103
|
+
);
|
|
104
|
+
super.visitMethodDeclaration(node);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Check if function/method is a widget builder
|
|
108
|
+
/// Return type contains "widget" (case insensitive) OR has widget tree pattern
|
|
109
|
+
bool _isWidgetBuilder(TypeAnnotation? returnType, FunctionBody body) {
|
|
110
|
+
final hasWidgetReturnType = returnType != null &&
|
|
111
|
+
returnType.toSource().toLowerCase().contains('widget');
|
|
112
|
+
|
|
113
|
+
final hasWidgetPattern = _hasNestedCapitalizedPattern(body);
|
|
114
|
+
|
|
115
|
+
return hasWidgetReturnType || hasWidgetPattern;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Count lines of logic code (excluding widget construction and comments)
|
|
119
|
+
int _countLogicLines(FunctionBody body) {
|
|
120
|
+
if (body is! BlockFunctionBody) {
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
final block = body.block;
|
|
125
|
+
final statements = block.statements;
|
|
126
|
+
|
|
127
|
+
int logicLineCount = 0;
|
|
128
|
+
|
|
129
|
+
for (final statement in statements) {
|
|
130
|
+
// Skip return statements (usually widget construction)
|
|
131
|
+
if (statement is ReturnStatement) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if statement is primarily widget construction
|
|
136
|
+
if (_isWidgetConstructionStatement(statement)) {
|
|
137
|
+
continue; // Skip widget construction
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Count lines for this statement
|
|
141
|
+
final startLine = analyzer.getLine(lineInfo, statement.offset);
|
|
142
|
+
final endLine = analyzer.getLine(lineInfo, statement.end);
|
|
143
|
+
final statementLines = endLine - startLine + 1;
|
|
144
|
+
|
|
145
|
+
logicLineCount += statementLines;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return logicLineCount;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Check if a statement is primarily widget construction
|
|
152
|
+
bool _isWidgetConstructionStatement(Statement statement) {
|
|
153
|
+
// Skip if statements that only contain return statements (early returns)
|
|
154
|
+
if (statement is IfStatement) {
|
|
155
|
+
return _isEarlyReturnIfStatement(statement);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Variable declarations that assign widgets
|
|
159
|
+
if (statement is VariableDeclarationStatement) {
|
|
160
|
+
final declarations = statement.variables.variables;
|
|
161
|
+
for (final decl in declarations) {
|
|
162
|
+
final initializer = decl.initializer;
|
|
163
|
+
if (initializer != null && _isWidgetExpression(initializer)) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Expression statements that are widget calls
|
|
170
|
+
if (statement is ExpressionStatement) {
|
|
171
|
+
return _isWidgetExpression(statement.expression);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// Check if an if statement only contains return statements (early return pattern)
|
|
178
|
+
bool _isEarlyReturnIfStatement(IfStatement ifStatement) {
|
|
179
|
+
// Check the then branch
|
|
180
|
+
final thenStatement = ifStatement.thenStatement;
|
|
181
|
+
if (!_isReturnOrWidgetBlock(thenStatement)) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check the else branch if it exists
|
|
186
|
+
final elseStatement = ifStatement.elseStatement;
|
|
187
|
+
if (elseStatement != null && !_isReturnOrWidgetBlock(elseStatement)) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// Check if a statement is a return statement or a block containing only returns
|
|
195
|
+
bool _isReturnOrWidgetBlock(Statement statement) {
|
|
196
|
+
if (statement is ReturnStatement) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (statement is Block) {
|
|
201
|
+
final statements = statement.statements;
|
|
202
|
+
// Empty block or all statements are returns
|
|
203
|
+
return statements.isEmpty ||
|
|
204
|
+
statements.every((s) => s is ReturnStatement);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Check if an expression is widget construction
|
|
211
|
+
bool _isWidgetExpression(Expression expression) {
|
|
212
|
+
if (expression is InstanceCreationExpression) {
|
|
213
|
+
final typeName = expression.constructorName.type.name2.lexeme;
|
|
214
|
+
if (typeName.isNotEmpty &&
|
|
215
|
+
_CapitalizedPatternVisitor._isCapitalized(typeName[0])) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (expression is MethodInvocation) {
|
|
221
|
+
final methodName = expression.methodName.name;
|
|
222
|
+
if (methodName.isNotEmpty &&
|
|
223
|
+
_CapitalizedPatternVisitor._isCapitalized(methodName[0])) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/// Detect nested capitalized identifiers (Flutter widget tree pattern)
|
|
232
|
+
/// Example: Container(child: Column(children: [Text(...), Icon(...)]))
|
|
233
|
+
bool _hasNestedCapitalizedPattern(FunctionBody body) {
|
|
234
|
+
final visitor = _CapitalizedPatternVisitor();
|
|
235
|
+
body.accept(visitor);
|
|
236
|
+
|
|
237
|
+
// If we have multiple capitalized identifiers with nesting depth >= 2
|
|
238
|
+
// it's likely a widget tree
|
|
239
|
+
return visitor.capitalizedCount >= 3 && visitor.maxNestingDepth >= 2;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
void _checkFunctionLength(
|
|
243
|
+
FunctionBody body,
|
|
244
|
+
String functionName,
|
|
245
|
+
int offset, {
|
|
246
|
+
required bool isBuildMethod,
|
|
247
|
+
required bool isWidgetBuilder,
|
|
248
|
+
}) {
|
|
249
|
+
final lineCount = _countEffectiveLines(body);
|
|
250
|
+
|
|
251
|
+
if (lineCount > maxLines) {
|
|
252
|
+
String message;
|
|
253
|
+
|
|
254
|
+
if (isBuildMethod) {
|
|
255
|
+
// Special message for build method with too much logic
|
|
256
|
+
message = 'Build method has too much logic ($lineCount lines, max $maxLines). Consider extracting logic into separate methods or widgets';
|
|
257
|
+
} else {
|
|
258
|
+
// Regular long function message
|
|
259
|
+
message = 'Function "$functionName" has $lineCount lines (max $maxLines). Consider breaking it into smaller functions';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
violations.add(analyzer.createViolation(
|
|
263
|
+
filePath: filePath,
|
|
264
|
+
line: analyzer.getLine(lineInfo, offset),
|
|
265
|
+
column: analyzer.getColumn(lineInfo, offset),
|
|
266
|
+
message: message,
|
|
267
|
+
));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
int _countEffectiveLines(FunctionBody body) {
|
|
272
|
+
// For expression functions like: () => expr
|
|
273
|
+
if (body is ExpressionFunctionBody) {
|
|
274
|
+
final startLine = analyzer.getLine(lineInfo, body.expression.offset);
|
|
275
|
+
final endLine = analyzer.getLine(lineInfo, body.expression.end);
|
|
276
|
+
return endLine - startLine + 1;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// For block functions like: () { ... }
|
|
280
|
+
if (body is BlockFunctionBody) {
|
|
281
|
+
final block = body.block;
|
|
282
|
+
final statements = block.statements;
|
|
283
|
+
|
|
284
|
+
// Empty function
|
|
285
|
+
if (statements.isEmpty) {
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Get first and last statement positions
|
|
290
|
+
final firstStatement = statements.first;
|
|
291
|
+
final lastStatement = statements.last;
|
|
292
|
+
|
|
293
|
+
final startLine = analyzer.getLine(lineInfo, firstStatement.offset);
|
|
294
|
+
final endLine = analyzer.getLine(lineInfo, lastStatement.end);
|
|
295
|
+
|
|
296
|
+
// Count lines from first statement to last statement (excluding opening/closing braces)
|
|
297
|
+
int totalLines = endLine - startLine + 1;
|
|
298
|
+
|
|
299
|
+
// Subtract comment lines
|
|
300
|
+
final commentLines = _countCommentLines(block, startLine, endLine);
|
|
301
|
+
|
|
302
|
+
return totalLines - commentLines;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Fallback: count total lines
|
|
306
|
+
final startLine = analyzer.getLine(lineInfo, body.offset);
|
|
307
|
+
final endLine = analyzer.getLine(lineInfo, body.end);
|
|
308
|
+
return endLine - startLine + 1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
int _countCommentLines(Block block, int startLine, int endLine) {
|
|
312
|
+
// Get the source code of the block
|
|
313
|
+
final source = block.toSource();
|
|
314
|
+
final lines = source.split('\n');
|
|
315
|
+
|
|
316
|
+
int commentLineCount = 0;
|
|
317
|
+
bool inMultiLineComment = false;
|
|
318
|
+
|
|
319
|
+
for (final line in lines) {
|
|
320
|
+
final trimmed = line.trim();
|
|
321
|
+
|
|
322
|
+
// Check for multi-line comment start
|
|
323
|
+
if (trimmed.contains('/*')) {
|
|
324
|
+
inMultiLineComment = true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// If in multi-line comment, count as comment line
|
|
328
|
+
if (inMultiLineComment) {
|
|
329
|
+
commentLineCount++;
|
|
330
|
+
// Check for multi-line comment end
|
|
331
|
+
if (trimmed.contains('*/')) {
|
|
332
|
+
inMultiLineComment = false;
|
|
333
|
+
}
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Check for single-line comment
|
|
338
|
+
if (trimmed.startsWith('//')) {
|
|
339
|
+
commentLineCount++;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Check for inline comment (code + comment on same line)
|
|
344
|
+
// Don't count as full comment line since there's code
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return commentLineCount;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/// Visitor to detect nested capitalized identifiers (Flutter widget pattern)
|
|
352
|
+
class _CapitalizedPatternVisitor extends RecursiveAstVisitor<void> {
|
|
353
|
+
int capitalizedCount = 0;
|
|
354
|
+
int maxNestingDepth = 0;
|
|
355
|
+
int _currentDepth = 0;
|
|
356
|
+
|
|
357
|
+
@override
|
|
358
|
+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
359
|
+
final typeName = node.constructorName.type.name2.lexeme;
|
|
360
|
+
|
|
361
|
+
// Check if type name starts with uppercase (widget pattern)
|
|
362
|
+
if (typeName.isNotEmpty && _isCapitalized(typeName[0])) {
|
|
363
|
+
capitalizedCount++;
|
|
364
|
+
_currentDepth++;
|
|
365
|
+
|
|
366
|
+
if (_currentDepth > maxNestingDepth) {
|
|
367
|
+
maxNestingDepth = _currentDepth;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
super.visitInstanceCreationExpression(node);
|
|
371
|
+
|
|
372
|
+
_currentDepth--;
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
super.visitInstanceCreationExpression(node);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
@override
|
|
380
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
381
|
+
final methodName = node.methodName.name;
|
|
382
|
+
|
|
383
|
+
// Check if method name starts with uppercase (could be a widget builder)
|
|
384
|
+
if (methodName.isNotEmpty && _isCapitalized(methodName[0])) {
|
|
385
|
+
capitalizedCount++;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
super.visitMethodInvocation(node);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
static bool _isCapitalized(String char) {
|
|
392
|
+
return char == char.toUpperCase() && char != char.toLowerCase();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
/// D009: Limit Function Parameters
|
|
10
|
+
/// Functions should not have too many parameters to maintain readability
|
|
11
|
+
class D009LimitFunctionParametersAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'D009';
|
|
14
|
+
|
|
15
|
+
// Default maximum number of parameters
|
|
16
|
+
static const int _defaultMaxParameters = 5;
|
|
17
|
+
|
|
18
|
+
// Default: skip nullable named parameters (they have implicit null default)
|
|
19
|
+
static const bool _defaultSkipNullableNamed = true;
|
|
20
|
+
|
|
21
|
+
@override
|
|
22
|
+
List<Violation> analyze({
|
|
23
|
+
required CompilationUnit unit,
|
|
24
|
+
required String filePath,
|
|
25
|
+
required Rule rule,
|
|
26
|
+
required LineInfo lineInfo,
|
|
27
|
+
}) {
|
|
28
|
+
final violations = <Violation>[];
|
|
29
|
+
|
|
30
|
+
// Get maxParameters from rule config, default to 5
|
|
31
|
+
final maxParameters =
|
|
32
|
+
(rule.config['maxParameters'] as int?) ?? _defaultMaxParameters;
|
|
33
|
+
|
|
34
|
+
// Get skipNullableNamedParameters from rule config, default to true
|
|
35
|
+
// Nullable named parameters have implicit null default, making them optional
|
|
36
|
+
final skipNullableNamed =
|
|
37
|
+
(rule.config['skipNullableNamedParameters'] as bool?) ??
|
|
38
|
+
_defaultSkipNullableNamed;
|
|
39
|
+
|
|
40
|
+
final visitor = _D009Visitor(
|
|
41
|
+
filePath: filePath,
|
|
42
|
+
lineInfo: lineInfo,
|
|
43
|
+
violations: violations,
|
|
44
|
+
analyzer: this,
|
|
45
|
+
maxParameters: maxParameters,
|
|
46
|
+
skipNullableNamed: skipNullableNamed,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
unit.accept(visitor);
|
|
50
|
+
|
|
51
|
+
return violations;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class _D009Visitor extends RecursiveAstVisitor<void> {
|
|
56
|
+
final String filePath;
|
|
57
|
+
final LineInfo lineInfo;
|
|
58
|
+
final List<Violation> violations;
|
|
59
|
+
final D009LimitFunctionParametersAnalyzer analyzer;
|
|
60
|
+
final int maxParameters;
|
|
61
|
+
final bool skipNullableNamed;
|
|
62
|
+
|
|
63
|
+
_D009Visitor({
|
|
64
|
+
required this.filePath,
|
|
65
|
+
required this.lineInfo,
|
|
66
|
+
required this.violations,
|
|
67
|
+
required this.analyzer,
|
|
68
|
+
required this.maxParameters,
|
|
69
|
+
required this.skipNullableNamed,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
@override
|
|
73
|
+
void visitFunctionDeclaration(FunctionDeclaration node) {
|
|
74
|
+
final parameters = node.functionExpression.parameters;
|
|
75
|
+
if (parameters != null) {
|
|
76
|
+
_checkParameterCount(
|
|
77
|
+
parameters,
|
|
78
|
+
node.name.lexeme,
|
|
79
|
+
'Function',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
super.visitFunctionDeclaration(node);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@override
|
|
86
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
87
|
+
final parameters = node.parameters;
|
|
88
|
+
if (parameters != null) {
|
|
89
|
+
_checkParameterCount(
|
|
90
|
+
parameters,
|
|
91
|
+
node.name.lexeme,
|
|
92
|
+
'Method',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
super.visitMethodDeclaration(node);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@override
|
|
99
|
+
void visitConstructorDeclaration(ConstructorDeclaration node) {
|
|
100
|
+
final parameters = node.parameters;
|
|
101
|
+
_checkParameterCount(
|
|
102
|
+
parameters,
|
|
103
|
+
node.name?.lexeme ?? '(unnamed constructor)',
|
|
104
|
+
'Constructor',
|
|
105
|
+
);
|
|
106
|
+
super.visitConstructorDeclaration(node);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
void _checkParameterCount(
|
|
110
|
+
FormalParameterList parameters,
|
|
111
|
+
String name,
|
|
112
|
+
String type,
|
|
113
|
+
) {
|
|
114
|
+
// Count parameters, excluding optional ones based on configuration
|
|
115
|
+
int paramCount = 0;
|
|
116
|
+
|
|
117
|
+
for (final param in parameters.parameters) {
|
|
118
|
+
// Check if it's a DefaultFormalParameter (can be named or positional with defaults)
|
|
119
|
+
if (param is DefaultFormalParameter) {
|
|
120
|
+
final isNamed = param.isNamed;
|
|
121
|
+
final hasExplicitDefault = param.defaultValue != null;
|
|
122
|
+
|
|
123
|
+
// Skip named parameters with explicit default values
|
|
124
|
+
if (isNamed && hasExplicitDefault) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Skip nullable named parameters if configured (they have implicit null default)
|
|
129
|
+
if (skipNullableNamed && isNamed && !hasExplicitDefault) {
|
|
130
|
+
// Check if the parameter type is nullable
|
|
131
|
+
final paramType = param.parameter;
|
|
132
|
+
if (_isNullableParameter(paramType)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
paramCount++;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (paramCount > maxParameters) {
|
|
142
|
+
violations.add(analyzer.createViolation(
|
|
143
|
+
filePath: filePath,
|
|
144
|
+
line: analyzer.getLine(lineInfo, parameters.offset),
|
|
145
|
+
column: analyzer.getColumn(lineInfo, parameters.offset),
|
|
146
|
+
message:
|
|
147
|
+
'$type "$name" has $paramCount parameters, exceeding the limit of $maxParameters. Consider refactoring or adjusting maxParameters/skipNullableNamedParameters in config if needed.',
|
|
148
|
+
));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Check if a parameter is nullable (has ? in type or uses dynamic/Object?)
|
|
153
|
+
bool _isNullableParameter(NormalFormalParameter param) {
|
|
154
|
+
if (param is SimpleFormalParameter) {
|
|
155
|
+
final type = param.type;
|
|
156
|
+
if (type == null) {
|
|
157
|
+
// No type annotation, could be dynamic
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check if type has question mark (e.g., String?, int?)
|
|
162
|
+
final typeSource = type.toSource();
|
|
163
|
+
return typeSource.endsWith('?');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (param is FieldFormalParameter) {
|
|
167
|
+
final type = param.type;
|
|
168
|
+
if (type == null) {
|
|
169
|
+
// Field formal parameter without explicit type
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
final typeSource = type.toSource();
|
|
174
|
+
return typeSource.endsWith('?');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|