@sun-asterisk/sunlint 1.3.43 → 1.3.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/dart_analyzer/README.md +226 -0
  2. package/dart_analyzer/analysis_options.yaml +66 -0
  3. package/dart_analyzer/bin/sunlint-dart-macos +0 -0
  4. package/dart_analyzer/bin/sunlint_dart_analyzer.dart +124 -0
  5. package/dart_analyzer/lib/analyzer_service.dart +625 -0
  6. package/dart_analyzer/lib/json_rpc_server.dart +275 -0
  7. package/dart_analyzer/lib/models/rule.dart +67 -0
  8. package/dart_analyzer/lib/models/symbol_table.dart +607 -0
  9. package/dart_analyzer/lib/models/violation.dart +69 -0
  10. package/dart_analyzer/lib/rules/base_analyzer.dart +52 -0
  11. package/dart_analyzer/lib/rules/common/C002_no_duplicate_code.dart +344 -0
  12. package/dart_analyzer/lib/rules/common/C003_no_vague_abbreviations.dart +318 -0
  13. package/dart_analyzer/lib/rules/common/C006_function_naming.dart +219 -0
  14. package/dart_analyzer/lib/rules/common/C008_variable_declaration_locality.dart +205 -0
  15. package/dart_analyzer/lib/rules/common/C010_limit_block_nesting.dart +162 -0
  16. package/dart_analyzer/lib/rules/common/C012_command_query_separation.dart +214 -0
  17. package/dart_analyzer/lib/rules/common/C013_no_dead_code.dart +225 -0
  18. package/dart_analyzer/lib/rules/common/C014_dependency_injection.dart +249 -0
  19. package/dart_analyzer/lib/rules/common/C017_constructor_logic.dart +158 -0
  20. package/dart_analyzer/lib/rules/common/C018_no_throw_generic_error.dart +141 -0
  21. package/dart_analyzer/lib/rules/common/C019_log_level_usage.dart +165 -0
  22. package/dart_analyzer/lib/rules/common/C020_unused_imports.dart +128 -0
  23. package/dart_analyzer/lib/rules/common/C021_import_organization.dart +86 -0
  24. package/dart_analyzer/lib/rules/common/C023_no_duplicate_variable.dart +112 -0
  25. package/dart_analyzer/lib/rules/common/C024_no_scatter_hardcoded_constants.dart +79 -0
  26. package/dart_analyzer/lib/rules/common/C029_catch_block_logging.dart +81 -0
  27. package/dart_analyzer/lib/rules/common/C030_use_custom_error_classes.dart +77 -0
  28. package/dart_analyzer/lib/rules/common/C031_validation_separation.dart +90 -0
  29. package/dart_analyzer/lib/rules/common/C033_separate_service_repository.dart +80 -0
  30. package/dart_analyzer/lib/rules/common/C035_error_logging_context.dart +148 -0
  31. package/dart_analyzer/lib/rules/common/C040_centralized_validation.dart +84 -0
  32. package/dart_analyzer/lib/rules/common/C041_no_sensitive_hardcode.dart +103 -0
  33. package/dart_analyzer/lib/rules/common/C042_boolean_name_prefix.dart +105 -0
  34. package/dart_analyzer/lib/rules/common/C043_no_console_or_print.dart +101 -0
  35. package/dart_analyzer/lib/rules/common/C047_no_duplicate_retry_logic.dart +94 -0
  36. package/dart_analyzer/lib/rules/common/C048_no_bypass_architectural_layers.dart +132 -0
  37. package/dart_analyzer/lib/rules/common/C052_parsing_or_data_transformation.dart +95 -0
  38. package/dart_analyzer/lib/rules/common/C060_no_override_superclass.dart +81 -0
  39. package/dart_analyzer/lib/rules/common/C065_one_behavior_per_test.dart +83 -0
  40. package/dart_analyzer/lib/rules/common/C067_no_hardcoded_config.dart +89 -0
  41. package/dart_analyzer/lib/rules/common/C070_no_real_time_tests.dart +99 -0
  42. package/dart_analyzer/lib/rules/common/C072_single_test_behavior.dart +78 -0
  43. package/dart_analyzer/lib/rules/common/C073_validate_required_config_on_startup.dart +82 -0
  44. package/dart_analyzer/lib/rules/common/C075_explicit_return_types.dart +85 -0
  45. package/dart_analyzer/lib/rules/common/C076_explicit_function_types.dart +104 -0
  46. package/dart_analyzer/lib/rules/dart/D001_recommended_lint_rules.dart +309 -0
  47. package/dart_analyzer/lib/rules/dart/D002_dispose_resources.dart +338 -0
  48. package/dart_analyzer/lib/rules/dart/D003_prefer_widgets_over_methods.dart +273 -0
  49. package/dart_analyzer/lib/rules/dart/D004_avoid_shrinkwrap_listview.dart +154 -0
  50. package/dart_analyzer/lib/rules/dart/D005_limit_widget_nesting.dart +265 -0
  51. package/dart_analyzer/lib/rules/dart/D006_prefer_extracting_large_callbacks.dart +135 -0
  52. package/dart_analyzer/lib/rules/dart/D007_prefer_init_first_dispose_last.dart +150 -0
  53. package/dart_analyzer/lib/rules/dart/D008_avoid_long_functions.dart +394 -0
  54. package/dart_analyzer/lib/rules/dart/D009_limit_function_parameters.dart +179 -0
  55. package/dart_analyzer/lib/rules/dart/D010_limit_cyclomatic_complexity.dart +257 -0
  56. package/dart_analyzer/lib/rules/dart/D011_prefer_named_parameters.dart +152 -0
  57. package/dart_analyzer/lib/rules/dart/D012_prefer_named_boolean_parameters.dart +156 -0
  58. package/dart_analyzer/lib/rules/dart/D013_single_public_class.dart +246 -0
  59. package/dart_analyzer/lib/rules/dart/D014_unsafe_collection_access.dart +202 -0
  60. package/dart_analyzer/lib/rules/dart/D015_copywith_all_parameters.dart +125 -0
  61. package/dart_analyzer/lib/rules/dart/D016_project_should_have_tests.dart +134 -0
  62. package/dart_analyzer/lib/rules/dart/D017_pubspec_dependencies_review.dart +187 -0
  63. package/dart_analyzer/lib/rules/dart/D018_remove_commented_code.dart +196 -0
  64. package/dart_analyzer/lib/rules/dart/D019_avoid_single_child_multi_child_widget.dart +161 -0
  65. package/dart_analyzer/lib/rules/dart/D020_limit_if_else_branches.dart +125 -0
  66. package/dart_analyzer/lib/rules/dart/D021_avoid_negated_boolean_checks.dart +227 -0
  67. package/dart_analyzer/lib/rules/dart/D022_use_setstate_correctly.dart +269 -0
  68. package/dart_analyzer/lib/rules/dart/D023_avoid_unnecessary_method_overrides.dart +191 -0
  69. package/dart_analyzer/lib/rules/dart/D024_avoid_unnecessary_stateful_widget.dart +194 -0
  70. package/dart_analyzer/lib/rules/dart/D025_avoid_nested_conditional_expressions.dart +90 -0
  71. package/dart_analyzer/lib/rules/security/S001_backend_auth_communications.dart +155 -0
  72. package/dart_analyzer/lib/rules/security/S002_os_command_injection.dart +159 -0
  73. package/dart_analyzer/lib/rules/security/S003_open_redirect_protection.dart +208 -0
  74. package/dart_analyzer/lib/rules/security/S004_sensitive_data_logging.dart +391 -0
  75. package/dart_analyzer/lib/rules/security/S005_trusted_service_authorization.dart +182 -0
  76. package/dart_analyzer/lib/rules/security/S006_no_default_credentials.dart +208 -0
  77. package/dart_analyzer/lib/rules/security/S007_output_encoding.dart +224 -0
  78. package/dart_analyzer/lib/rules/security/S008_svg_content_sanitization.dart +211 -0
  79. package/dart_analyzer/lib/rules/security/S009_no_insecure_encryption.dart +160 -0
  80. package/dart_analyzer/lib/rules/security/S010_use_csprng.dart +184 -0
  81. package/dart_analyzer/lib/rules/security/S011_ech_tls_config.dart +175 -0
  82. package/dart_analyzer/lib/rules/security/S012_hardcoded_secrets.dart +255 -0
  83. package/dart_analyzer/lib/rules/security/S013_tls_enforcement.dart +148 -0
  84. package/dart_analyzer/lib/rules/security/S014_tls_version_enforcement.dart +117 -0
  85. package/dart_analyzer/lib/rules/security/S015_insecure_tls_certificate.dart +315 -0
  86. package/dart_analyzer/lib/rules/security/S016_no_sensitive_querystring.dart +244 -0
  87. package/dart_analyzer/lib/rules/security/S017_use_parameterized_queries.dart +191 -0
  88. package/dart_analyzer/lib/rules/security/S018_no_sensitive_browser_storage.dart +175 -0
  89. package/dart_analyzer/lib/rules/security/S019_smtp_injection_protection.dart +166 -0
  90. package/dart_analyzer/lib/rules/security/S020_no_eval_dynamic_code.dart +149 -0
  91. package/dart_analyzer/lib/rules/security/S021_referrer_policy.dart +146 -0
  92. package/dart_analyzer/lib/rules/security/S022_escape_output_context.dart +111 -0
  93. package/dart_analyzer/lib/rules/security/S023_no_json_injection.dart +550 -0
  94. package/dart_analyzer/lib/rules/security/S024_xpath_xxe_protection.dart +299 -0
  95. package/dart_analyzer/lib/rules/security/S025_server_side_validation.dart +140 -0
  96. package/dart_analyzer/lib/rules/security/S026_tls_all_connections.dart +196 -0
  97. package/dart_analyzer/lib/rules/security/S027_mtls_certificate_validation.dart +195 -0
  98. package/dart_analyzer/lib/rules/security/S028_file_upload_size_limits.dart +186 -0
  99. package/dart_analyzer/lib/rules/security/S029_csrf_protection.dart +171 -0
  100. package/dart_analyzer/lib/rules/security/S030_directory_browsing_protection.dart +144 -0
  101. package/dart_analyzer/lib/rules/security/S031_secure_session_cookies.dart +118 -0
  102. package/dart_analyzer/lib/rules/security/S032_httponly_session_cookies.dart +114 -0
  103. package/dart_analyzer/lib/rules/security/S033_samesite_session_cookies.dart +120 -0
  104. package/dart_analyzer/lib/rules/security/S034_host_prefix_session_cookies.dart +160 -0
  105. package/dart_analyzer/lib/rules/security/S035_separate_app_hostnames.dart +117 -0
  106. package/dart_analyzer/lib/rules/security/S036_lfi_rfi_protection.dart +188 -0
  107. package/dart_analyzer/lib/rules/security/S037_cache_headers.dart +113 -0
  108. package/dart_analyzer/lib/rules/security/S038_no_version_headers.dart +114 -0
  109. package/dart_analyzer/lib/rules/security/S039_tls_certificate_validation.dart +131 -0
  110. package/dart_analyzer/lib/rules/security/S040_session_fixation_protection.dart +155 -0
  111. package/dart_analyzer/lib/rules/security/S041_session_token_invalidation.dart +201 -0
  112. package/dart_analyzer/lib/rules/security/S042_require_re_authentication_for_long_lived.dart +158 -0
  113. package/dart_analyzer/lib/rules/security/S043_password_changes_invalidate_all_sessions.dart +88 -0
  114. package/dart_analyzer/lib/rules/security/S044_re_authentication_required.dart +119 -0
  115. package/dart_analyzer/lib/rules/security/S045_brute_force_protection.dart +253 -0
  116. package/dart_analyzer/lib/rules/security/S046_jwt_algorithm_allowlist.dart +113 -0
  117. package/dart_analyzer/lib/rules/security/S047_oauth_pkce_protection.dart +124 -0
  118. package/dart_analyzer/lib/rules/security/S048_oauth_redirect_uri_validation.dart +134 -0
  119. package/dart_analyzer/lib/rules/security/S049_short_validity_tokens.dart +145 -0
  120. package/dart_analyzer/lib/rules/security/S050_reference_tokens_entropy.dart +234 -0
  121. package/dart_analyzer/lib/rules/security/S051_password_length_policy.dart +171 -0
  122. package/dart_analyzer/lib/rules/security/S052_weak_otp_entropy.dart +107 -0
  123. package/dart_analyzer/lib/rules/security/S053_generic_error_messages.dart +159 -0
  124. package/dart_analyzer/lib/rules/security/S054_no_default_accounts.dart +141 -0
  125. package/dart_analyzer/lib/rules/security/S055_content_type_validation.dart +324 -0
  126. package/dart_analyzer/lib/rules/security/S056_log_injection_protection.dart +119 -0
  127. package/dart_analyzer/lib/rules/security/S057_utc_logging.dart +114 -0
  128. package/dart_analyzer/lib/rules/security/S058_no_ssrf.dart +175 -0
  129. package/dart_analyzer/lib/rules/security/S059_disable_debug_mode.dart +172 -0
  130. package/dart_analyzer/lib/rules/security/S060_password_minimum_length.dart +170 -0
  131. package/dart_analyzer/lib/symbol_table_extractor.dart +510 -0
  132. package/dart_analyzer/lib/utils/common_utils.dart +26 -0
  133. package/dart_analyzer/pubspec.lock +557 -0
  134. package/dart_analyzer/pubspec.yaml +39 -0
  135. package/dart_analyzer/test/fixtures/complex_code.dart +95 -0
  136. package/docs/GENERATED_FILE_HANDLING_SUMMARY.md +2 -2
  137. package/package.json +3 -2
@@ -0,0 +1,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
+ }