@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,154 @@
|
|
|
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
|
+
/// D004: Avoid shrinkWrap in ListView
|
|
10
|
+
/// ListView with shrinkWrap: true causes performance issues by disabling lazy loading
|
|
11
|
+
/// and forcing all items to render at once. Use Expanded/Flexible or SliverList instead.
|
|
12
|
+
class D004AvoidShrinkwrapListviewAnalyzer extends BaseAnalyzer {
|
|
13
|
+
@override
|
|
14
|
+
String get ruleId => 'D004';
|
|
15
|
+
|
|
16
|
+
// Widget types to check for shrinkWrap usage
|
|
17
|
+
static const _scrollableWidgets = [
|
|
18
|
+
'ListView',
|
|
19
|
+
'GridView',
|
|
20
|
+
'ListView.builder',
|
|
21
|
+
'ListView.separated',
|
|
22
|
+
'GridView.builder',
|
|
23
|
+
'GridView.count',
|
|
24
|
+
'GridView.extent',
|
|
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 = _D004Visitor(
|
|
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 _D004Visitor extends RecursiveAstVisitor<void> {
|
|
50
|
+
final String filePath;
|
|
51
|
+
final LineInfo lineInfo;
|
|
52
|
+
final List<Violation> violations;
|
|
53
|
+
final D004AvoidShrinkwrapListviewAnalyzer analyzer;
|
|
54
|
+
|
|
55
|
+
_D004Visitor({
|
|
56
|
+
required this.filePath,
|
|
57
|
+
required this.lineInfo,
|
|
58
|
+
required this.violations,
|
|
59
|
+
required this.analyzer,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
@override
|
|
63
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
64
|
+
// Handle named constructors like ListView.builder, GridView.count, etc.
|
|
65
|
+
// In Dart AST, these are MethodInvocation nodes, not InstanceCreationExpression
|
|
66
|
+
final target = node.target;
|
|
67
|
+
final methodName = node.methodName.name;
|
|
68
|
+
|
|
69
|
+
if (target != null) {
|
|
70
|
+
final targetName = target.toString();
|
|
71
|
+
final fullName = '$targetName.$methodName';
|
|
72
|
+
|
|
73
|
+
if (_isScrollableWidget(fullName) || _isScrollableWidget(targetName)) {
|
|
74
|
+
_checkForShrinkWrap(node.argumentList, fullName);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
super.visitMethodInvocation(node);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@override
|
|
82
|
+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
83
|
+
final typeName = node.constructorName.type.toSource();
|
|
84
|
+
// Check if this is a ListView or GridView construction
|
|
85
|
+
if (_isScrollableWidget(typeName)) {
|
|
86
|
+
_checkForShrinkWrap(node.argumentList, typeName);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
super.visitInstanceCreationExpression(node);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Check for shrinkWrap parameter in argument list
|
|
93
|
+
void _checkForShrinkWrap(ArgumentList argumentList, String widgetName) {
|
|
94
|
+
final arguments = argumentList.arguments;
|
|
95
|
+
for (final arg in arguments) {
|
|
96
|
+
if (arg is NamedExpression) {
|
|
97
|
+
final paramName = arg.name.label.name;
|
|
98
|
+
if (paramName == 'shrinkWrap') {
|
|
99
|
+
// Check if the value is true
|
|
100
|
+
final expression = arg.expression;
|
|
101
|
+
|
|
102
|
+
if (_isTrueValue(expression)) {
|
|
103
|
+
violations.add(analyzer.createViolation(
|
|
104
|
+
filePath: filePath,
|
|
105
|
+
line: analyzer.getLine(lineInfo, arg.offset),
|
|
106
|
+
column: analyzer.getColumn(lineInfo, arg.offset),
|
|
107
|
+
message: 'Avoid using shrinkWrap: true in $widgetName. '
|
|
108
|
+
'It disables lazy loading and forces all items to render at once, '
|
|
109
|
+
'causing performance issues. Use Expanded/Flexible widget or SliverList instead.',
|
|
110
|
+
));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Check if the type name matches a scrollable widget
|
|
118
|
+
bool _isScrollableWidget(String typeName) {
|
|
119
|
+
return D004AvoidShrinkwrapListviewAnalyzer._scrollableWidgets.any(
|
|
120
|
+
(widget) => typeName.startsWith(widget),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Check if an expression evaluates to true
|
|
125
|
+
bool _isTrueValue(Expression expression) {
|
|
126
|
+
// Direct boolean literal: true
|
|
127
|
+
if (expression is BooleanLiteral) {
|
|
128
|
+
return expression.value;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Simple identifier: could be a constant or variable
|
|
132
|
+
// We flag it as potential violation to be safe
|
|
133
|
+
if (expression is SimpleIdentifier) {
|
|
134
|
+
final name = expression.name;
|
|
135
|
+
// Common patterns for true values
|
|
136
|
+
if (name == 'true' || name.toLowerCase().contains('true')) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Prefix expression: !false
|
|
142
|
+
if (expression is PrefixExpression) {
|
|
143
|
+
if (expression.operator.lexeme == '!') {
|
|
144
|
+
// If it's !false, that's true
|
|
145
|
+
if (expression.operand is BooleanLiteral) {
|
|
146
|
+
final literal = expression.operand as BooleanLiteral;
|
|
147
|
+
return !literal.value;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
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
|
+
/// D005: Limit Widget Nesting Depth
|
|
10
|
+
/// Widget nesting should not exceed 6 levels to maintain code readability and performance.
|
|
11
|
+
/// Deeply nested widgets make code harder to understand and can impact performance.
|
|
12
|
+
class D005LimitWidgetNestingAnalyzer extends BaseAnalyzer {
|
|
13
|
+
@override
|
|
14
|
+
String get ruleId => 'D005';
|
|
15
|
+
|
|
16
|
+
static const int _maxNestingDepth = 6;
|
|
17
|
+
|
|
18
|
+
// Common widget types to check for nesting
|
|
19
|
+
static const _widgetTypes = [
|
|
20
|
+
'Container',
|
|
21
|
+
'Column',
|
|
22
|
+
'Row',
|
|
23
|
+
'Stack',
|
|
24
|
+
'Scaffold',
|
|
25
|
+
'Card',
|
|
26
|
+
'Padding',
|
|
27
|
+
'Center',
|
|
28
|
+
'Align',
|
|
29
|
+
'Expanded',
|
|
30
|
+
'Flexible',
|
|
31
|
+
'SizedBox',
|
|
32
|
+
'ListView',
|
|
33
|
+
'GridView',
|
|
34
|
+
'SingleChildScrollView',
|
|
35
|
+
'CustomScrollView',
|
|
36
|
+
'Wrap',
|
|
37
|
+
'AnimatedContainer',
|
|
38
|
+
'DecoratedBox',
|
|
39
|
+
'Transform',
|
|
40
|
+
'Positioned',
|
|
41
|
+
'SafeArea',
|
|
42
|
+
'Material',
|
|
43
|
+
'InkWell',
|
|
44
|
+
'GestureDetector',
|
|
45
|
+
'Text',
|
|
46
|
+
'Icon',
|
|
47
|
+
'Image',
|
|
48
|
+
'CircleAvatar',
|
|
49
|
+
'TextField',
|
|
50
|
+
'Button',
|
|
51
|
+
'ElevatedButton',
|
|
52
|
+
'TextButton',
|
|
53
|
+
'OutlinedButton',
|
|
54
|
+
'IconButton',
|
|
55
|
+
'FloatingActionButton',
|
|
56
|
+
'AppBar',
|
|
57
|
+
'BottomNavigationBar',
|
|
58
|
+
'Drawer',
|
|
59
|
+
'ListTile',
|
|
60
|
+
'CheckboxListTile',
|
|
61
|
+
'RadioListTile',
|
|
62
|
+
'SwitchListTile',
|
|
63
|
+
'Chip',
|
|
64
|
+
'InputChip',
|
|
65
|
+
'ChoiceChip',
|
|
66
|
+
'FilterChip',
|
|
67
|
+
'ActionChip',
|
|
68
|
+
'Tooltip',
|
|
69
|
+
'Banner',
|
|
70
|
+
'SnackBar',
|
|
71
|
+
'Dialog',
|
|
72
|
+
'AlertDialog',
|
|
73
|
+
'SimpleDialog',
|
|
74
|
+
'BottomSheet',
|
|
75
|
+
'DatePicker',
|
|
76
|
+
'TimePicker',
|
|
77
|
+
'Slider',
|
|
78
|
+
'Switch',
|
|
79
|
+
'Checkbox',
|
|
80
|
+
'Radio',
|
|
81
|
+
'DropdownButton',
|
|
82
|
+
'PopupMenuButton',
|
|
83
|
+
'Tab',
|
|
84
|
+
'TabBar',
|
|
85
|
+
'TabBarView',
|
|
86
|
+
'Stepper',
|
|
87
|
+
'Step',
|
|
88
|
+
'ExpansionPanel',
|
|
89
|
+
'ExpansionTile',
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
@override
|
|
93
|
+
List<Violation> analyze({
|
|
94
|
+
required CompilationUnit unit,
|
|
95
|
+
required String filePath,
|
|
96
|
+
required Rule rule,
|
|
97
|
+
required LineInfo lineInfo,
|
|
98
|
+
}) {
|
|
99
|
+
final violations = <Violation>[];
|
|
100
|
+
|
|
101
|
+
final visitor = _D005Visitor(
|
|
102
|
+
filePath: filePath,
|
|
103
|
+
lineInfo: lineInfo,
|
|
104
|
+
violations: violations,
|
|
105
|
+
analyzer: this,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
unit.accept(visitor);
|
|
109
|
+
|
|
110
|
+
return violations;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
class _D005Visitor extends RecursiveAstVisitor<void> {
|
|
115
|
+
final String filePath;
|
|
116
|
+
final LineInfo lineInfo;
|
|
117
|
+
final List<Violation> violations;
|
|
118
|
+
final D005LimitWidgetNestingAnalyzer analyzer;
|
|
119
|
+
|
|
120
|
+
_D005Visitor({
|
|
121
|
+
required this.filePath,
|
|
122
|
+
required this.lineInfo,
|
|
123
|
+
required this.violations,
|
|
124
|
+
required this.analyzer,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Track widget nesting depth for each build method
|
|
128
|
+
int _currentDepth = 0;
|
|
129
|
+
int _maxDepthInCurrentMethod = 0;
|
|
130
|
+
bool _insideBuildMethod = false;
|
|
131
|
+
|
|
132
|
+
@override
|
|
133
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
134
|
+
// Check if this is a build method
|
|
135
|
+
if (node.name.lexeme == 'build') {
|
|
136
|
+
// Reset counters for new build method
|
|
137
|
+
_currentDepth = 0;
|
|
138
|
+
_maxDepthInCurrentMethod = 0;
|
|
139
|
+
_insideBuildMethod = true;
|
|
140
|
+
|
|
141
|
+
// Visit the method body
|
|
142
|
+
super.visitMethodDeclaration(node);
|
|
143
|
+
|
|
144
|
+
_insideBuildMethod = false;
|
|
145
|
+
|
|
146
|
+
// Check if max depth exceeded
|
|
147
|
+
if (_maxDepthInCurrentMethod > D005LimitWidgetNestingAnalyzer._maxNestingDepth) {
|
|
148
|
+
violations.add(analyzer.createViolation(
|
|
149
|
+
filePath: filePath,
|
|
150
|
+
line: analyzer.getLine(lineInfo, node.name.offset),
|
|
151
|
+
column: analyzer.getColumn(lineInfo, node.name.offset),
|
|
152
|
+
message:
|
|
153
|
+
'Widget nesting depth is $_maxDepthInCurrentMethod, which exceeds the maximum of ${D005LimitWidgetNestingAnalyzer._maxNestingDepth}. '
|
|
154
|
+
'Consider extracting nested widgets into separate widget classes to improve readability and maintainability.',
|
|
155
|
+
));
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
super.visitMethodDeclaration(node);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@override
|
|
163
|
+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
164
|
+
if (!_insideBuildMethod) {
|
|
165
|
+
super.visitInstanceCreationExpression(node);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
final typeName = node.constructorName.type.toSource();
|
|
170
|
+
|
|
171
|
+
// Check if this is a widget instance
|
|
172
|
+
if (_isWidget(typeName)) {
|
|
173
|
+
_currentDepth++;
|
|
174
|
+
if (_currentDepth > _maxDepthInCurrentMethod) {
|
|
175
|
+
_maxDepthInCurrentMethod = _currentDepth;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
super.visitInstanceCreationExpression(node);
|
|
180
|
+
|
|
181
|
+
if (_isWidget(typeName)) {
|
|
182
|
+
_currentDepth--;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@override
|
|
187
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
188
|
+
if (!_insideBuildMethod) {
|
|
189
|
+
super.visitMethodInvocation(node);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
final methodName = node.methodName.name;
|
|
194
|
+
|
|
195
|
+
// Handle widget constructor calls (no target, starts with uppercase)
|
|
196
|
+
// In modern Dart, widget constructors without 'new' are parsed as MethodInvocation
|
|
197
|
+
if (node.target == null) {
|
|
198
|
+
if (methodName.isNotEmpty && _isWidget(methodName)) {
|
|
199
|
+
_currentDepth++;
|
|
200
|
+
if (_currentDepth > _maxDepthInCurrentMethod) {
|
|
201
|
+
_maxDepthInCurrentMethod = _currentDepth;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
super.visitMethodInvocation(node);
|
|
205
|
+
|
|
206
|
+
_currentDepth--;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Handle named constructors like ListView.builder, GridView.count
|
|
212
|
+
final target = node.target;
|
|
213
|
+
if (target != null) {
|
|
214
|
+
final targetName = target.toString();
|
|
215
|
+
final fullName = '$targetName.$methodName';
|
|
216
|
+
|
|
217
|
+
if (_isWidget(fullName) || _isWidget(targetName)) {
|
|
218
|
+
_currentDepth++;
|
|
219
|
+
if (_currentDepth > _maxDepthInCurrentMethod) {
|
|
220
|
+
_maxDepthInCurrentMethod = _currentDepth;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
super.visitMethodInvocation(node);
|
|
226
|
+
|
|
227
|
+
if (target != null) {
|
|
228
|
+
final targetName = target.toString();
|
|
229
|
+
final fullName = '$targetName.$methodName';
|
|
230
|
+
|
|
231
|
+
if (_isWidget(fullName) || _isWidget(targetName)) {
|
|
232
|
+
_currentDepth--;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// Check if the type name represents a widget
|
|
238
|
+
/// We check by looking at common patterns rather than exhaustive list
|
|
239
|
+
bool _isWidget(String typeName) {
|
|
240
|
+
// Known widget types from our list
|
|
241
|
+
if (D005LimitWidgetNestingAnalyzer._widgetTypes.any(
|
|
242
|
+
(widget) => typeName.startsWith(widget),
|
|
243
|
+
)) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Additional heuristics:
|
|
248
|
+
// 1. Ends with common widget suffixes
|
|
249
|
+
if (typeName.endsWith('Widget') ||
|
|
250
|
+
typeName.endsWith('View') ||
|
|
251
|
+
typeName.endsWith('Screen') ||
|
|
252
|
+
typeName.endsWith('Page')) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 2. Starts with common Flutter widget prefixes
|
|
257
|
+
if (typeName.startsWith('Cupertino') ||
|
|
258
|
+
typeName.startsWith('Material') ||
|
|
259
|
+
typeName.startsWith('Adaptive')) {
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
/// D006: Prefer Extracting Large Callbacks from Build
|
|
10
|
+
/// Large callback functions in widget builders should be extracted to separate methods
|
|
11
|
+
/// for better readability and maintainability.
|
|
12
|
+
class D006PreferExtractingLargeCallbacksAnalyzer extends BaseAnalyzer {
|
|
13
|
+
@override
|
|
14
|
+
String get ruleId => 'D006';
|
|
15
|
+
|
|
16
|
+
static const int _maxLines = 5;
|
|
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
|
+
final visitor = _D006Visitor(
|
|
28
|
+
filePath: filePath,
|
|
29
|
+
lineInfo: lineInfo,
|
|
30
|
+
violations: violations,
|
|
31
|
+
analyzer: this,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
unit.accept(visitor);
|
|
35
|
+
|
|
36
|
+
return violations;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class _D006Visitor extends RecursiveAstVisitor<void> {
|
|
41
|
+
final String filePath;
|
|
42
|
+
final LineInfo lineInfo;
|
|
43
|
+
final List<Violation> violations;
|
|
44
|
+
final D006PreferExtractingLargeCallbacksAnalyzer analyzer;
|
|
45
|
+
|
|
46
|
+
_D006Visitor({
|
|
47
|
+
required this.filePath,
|
|
48
|
+
required this.lineInfo,
|
|
49
|
+
required this.violations,
|
|
50
|
+
required this.analyzer,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
bool _insideBuildMethod = false;
|
|
54
|
+
|
|
55
|
+
@override
|
|
56
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
57
|
+
// Track if we're inside a build method
|
|
58
|
+
final wasPreviouslyInsideBuild = _insideBuildMethod;
|
|
59
|
+
if (node.name.lexeme == 'build') {
|
|
60
|
+
_insideBuildMethod = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
super.visitMethodDeclaration(node);
|
|
64
|
+
|
|
65
|
+
_insideBuildMethod = wasPreviouslyInsideBuild;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@override
|
|
69
|
+
void visitNamedExpression(NamedExpression node) {
|
|
70
|
+
if (!_insideBuildMethod) {
|
|
71
|
+
super.visitNamedExpression(node);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
final propertyName = node.name.label.name;
|
|
76
|
+
final expression = node.expression;
|
|
77
|
+
|
|
78
|
+
// Check ANY function expression (anonymous function/lambda) regardless of name
|
|
79
|
+
// This catches all inline callbacks: onTap, onPressed, custom callbacks, etc.
|
|
80
|
+
if (expression is FunctionExpression) {
|
|
81
|
+
final lineCount = _countLines(expression);
|
|
82
|
+
|
|
83
|
+
if (lineCount > D006PreferExtractingLargeCallbacksAnalyzer._maxLines) {
|
|
84
|
+
violations.add(analyzer.createViolation(
|
|
85
|
+
filePath: filePath,
|
|
86
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
87
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
88
|
+
message:
|
|
89
|
+
'Inline function "$propertyName" has $lineCount lines (max ${D006PreferExtractingLargeCallbacksAnalyzer._maxLines}). '
|
|
90
|
+
'Consider extracting it to a separate method for better readability and testability.',
|
|
91
|
+
));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
super.visitNamedExpression(node);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Count the number of lines in a function expression
|
|
99
|
+
/// Ignores the opening and closing braces to count only actual code lines
|
|
100
|
+
int _countLines(FunctionExpression node) {
|
|
101
|
+
final body = node.body;
|
|
102
|
+
|
|
103
|
+
// For expression functions like: () => expression
|
|
104
|
+
if (body is ExpressionFunctionBody) {
|
|
105
|
+
final startLine = analyzer.getLine(lineInfo, body.expression.offset);
|
|
106
|
+
final endLine = analyzer.getLine(lineInfo, body.expression.end);
|
|
107
|
+
return endLine - startLine + 1;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// For block functions like: () { ... }
|
|
111
|
+
if (body is BlockFunctionBody) {
|
|
112
|
+
final block = body.block;
|
|
113
|
+
final statements = block.statements;
|
|
114
|
+
|
|
115
|
+
// If no statements, it's empty - count as 0
|
|
116
|
+
if (statements.isEmpty) {
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Count from first statement to last statement, ignoring { and }
|
|
121
|
+
final firstStatement = statements.first;
|
|
122
|
+
final lastStatement = statements.last;
|
|
123
|
+
|
|
124
|
+
final startLine = analyzer.getLine(lineInfo, firstStatement.offset);
|
|
125
|
+
final endLine = analyzer.getLine(lineInfo, lastStatement.end);
|
|
126
|
+
|
|
127
|
+
return endLine - startLine + 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Fallback: count total lines (shouldn't normally reach here)
|
|
131
|
+
final startLine = analyzer.getLine(lineInfo, node.offset);
|
|
132
|
+
final endLine = analyzer.getLine(lineInfo, node.end);
|
|
133
|
+
return endLine - startLine + 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
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
|
+
/// D007: Prefer Init First, Dispose Last
|
|
10
|
+
/// Code should be called after super.initState() and before super.dispose()
|
|
11
|
+
class D007PreferInitFirstDisposeLastAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'D007';
|
|
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 = _D007Visitor(
|
|
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 _D007Visitor extends RecursiveAstVisitor<void> {
|
|
38
|
+
final String filePath;
|
|
39
|
+
final LineInfo lineInfo;
|
|
40
|
+
final List<Violation> violations;
|
|
41
|
+
final D007PreferInitFirstDisposeLastAnalyzer analyzer;
|
|
42
|
+
|
|
43
|
+
_D007Visitor({
|
|
44
|
+
required this.filePath,
|
|
45
|
+
required this.lineInfo,
|
|
46
|
+
required this.violations,
|
|
47
|
+
required this.analyzer,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
@override
|
|
51
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
52
|
+
final methodName = node.name.lexeme;
|
|
53
|
+
|
|
54
|
+
if (methodName == 'initState') {
|
|
55
|
+
_checkInitState(node);
|
|
56
|
+
} else if (methodName == 'dispose') {
|
|
57
|
+
_checkDispose(node);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
super.visitMethodDeclaration(node);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
void _checkInitState(MethodDeclaration node) {
|
|
64
|
+
final body = node.body;
|
|
65
|
+
if (body is! BlockFunctionBody) return;
|
|
66
|
+
|
|
67
|
+
final block = body.block;
|
|
68
|
+
final statements = block.statements;
|
|
69
|
+
if (statements.isEmpty) return;
|
|
70
|
+
|
|
71
|
+
// Find super.initState() call
|
|
72
|
+
int superInitStateIndex = -1;
|
|
73
|
+
for (int i = 0; i < statements.length; i++) {
|
|
74
|
+
if (_isSuperInitStateCall(statements[i])) {
|
|
75
|
+
superInitStateIndex = i;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// If no super.initState() found, don't check
|
|
81
|
+
if (superInitStateIndex == -1) return;
|
|
82
|
+
|
|
83
|
+
// If super.initState() is not first, report violation
|
|
84
|
+
if (superInitStateIndex > 0) {
|
|
85
|
+
final firstStatement = statements[0];
|
|
86
|
+
violations.add(analyzer.createViolation(
|
|
87
|
+
filePath: filePath,
|
|
88
|
+
line: analyzer.getLine(lineInfo, firstStatement.offset),
|
|
89
|
+
column: analyzer.getColumn(lineInfo, firstStatement.offset),
|
|
90
|
+
message: 'super.initState() should be called first in initState(). Move code after super.initState()',
|
|
91
|
+
));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
void _checkDispose(MethodDeclaration node) {
|
|
96
|
+
final body = node.body;
|
|
97
|
+
if (body is! BlockFunctionBody) return;
|
|
98
|
+
|
|
99
|
+
final block = body.block;
|
|
100
|
+
final statements = block.statements;
|
|
101
|
+
if (statements.isEmpty) return;
|
|
102
|
+
|
|
103
|
+
// Find super.dispose() call
|
|
104
|
+
int superDisposeIndex = -1;
|
|
105
|
+
for (int i = 0; i < statements.length; i++) {
|
|
106
|
+
if (_isSuperDisposeCall(statements[i])) {
|
|
107
|
+
superDisposeIndex = i;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// If no super.dispose() found, don't check
|
|
113
|
+
if (superDisposeIndex == -1) return;
|
|
114
|
+
|
|
115
|
+
// If super.dispose() is not last, report violation
|
|
116
|
+
if (superDisposeIndex < statements.length - 1) {
|
|
117
|
+
final statementAfterSuper = statements[superDisposeIndex + 1];
|
|
118
|
+
violations.add(analyzer.createViolation(
|
|
119
|
+
filePath: filePath,
|
|
120
|
+
line: analyzer.getLine(lineInfo, statementAfterSuper.offset),
|
|
121
|
+
column: analyzer.getColumn(lineInfo, statementAfterSuper.offset),
|
|
122
|
+
message: 'super.dispose() should be called last in dispose(). Move code before super.dispose()',
|
|
123
|
+
));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
bool _isSuperInitStateCall(Statement statement) {
|
|
128
|
+
if (statement is! ExpressionStatement) return false;
|
|
129
|
+
|
|
130
|
+
final expression = statement.expression;
|
|
131
|
+
if (expression is! MethodInvocation) return false;
|
|
132
|
+
|
|
133
|
+
final target = expression.target;
|
|
134
|
+
if (target is! SuperExpression) return false;
|
|
135
|
+
|
|
136
|
+
return expression.methodName.name == 'initState';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
bool _isSuperDisposeCall(Statement statement) {
|
|
140
|
+
if (statement is! ExpressionStatement) return false;
|
|
141
|
+
|
|
142
|
+
final expression = statement.expression;
|
|
143
|
+
if (expression is! MethodInvocation) return false;
|
|
144
|
+
|
|
145
|
+
final target = expression.target;
|
|
146
|
+
if (target is! SuperExpression) return false;
|
|
147
|
+
|
|
148
|
+
return expression.methodName.name == 'dispose';
|
|
149
|
+
}
|
|
150
|
+
}
|