Skip to content

Commit 8d0197e

Browse files
solid-illiaaihistovIllia Aihistov
andauthored
fix: resolve false positives in avoid_returning_widgets (#338)
* fix: resolve false positives in avoid_returning_widgets * refactor: use _isFlutterType helper for accuracy and update avoid_returning_widgets rule to target State.widget accessors * fix: avoid_returning_widgets restrict state widget casting exclusion to super/this receivers --------- Co-authored-by: Illia Aihistov <illia.aihistov-us@solid.software>
1 parent 06b5d55 commit 8d0197e

4 files changed

Lines changed: 187 additions & 59 deletions

File tree

lib/src/lints/avoid_returning_widgets/visitors/avoid_returning_widgets_visitor.dart

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import 'package:analyzer/dart/ast/ast.dart';
22
import 'package:analyzer/dart/ast/visitor.dart';
33
import 'package:analyzer/dart/element/element.dart';
4-
import 'package:analyzer/dart/element/type.dart';
54
import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart';
65
import 'package:solid_lints/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart';
6+
import 'package:solid_lints/src/utils/node_utils.dart';
77
import 'package:solid_lints/src/utils/types_utils.dart';
88

99
/// A visitor that reports on functions that return widgets.
@@ -40,20 +40,23 @@ class AvoidReturningWidgetsVisitor extends RecursiveAstVisitor<void> {
4040
return;
4141
}
4242

43+
if (node is MethodDeclaration &&
44+
(node.isAbstract ||
45+
node.body is EmptyFunctionBody ||
46+
(node.isGetter && _isStateWidgetCastingGetter(node)))) {
47+
return;
48+
}
49+
4350
final returnType = switch (node) {
44-
Declaration(
45-
declaredFragment: ExecutableFragment(
46-
element: ExecutableElement(type: FunctionType(:final returnType)),
47-
),
48-
) =>
49-
returnType,
50-
MethodDeclaration(returnType: TypeAnnotation(:final type)) => type,
51-
FunctionDeclaration(returnType: TypeAnnotation(:final type)) => type,
51+
MethodDeclaration(:final declaredFragment?) =>
52+
declaredFragment.element.returnType,
53+
FunctionDeclaration(:final declaredFragment?) =>
54+
declaredFragment.element.returnType,
5255
_ => null,
5356
};
5457
if (returnType == null) return;
5558

56-
final isWidgetReturned = hasWidgetType(returnType);
59+
final isWidgetReturned = isWidgetType(returnType);
5760
if (!isWidgetReturned) return;
5861

5962
final isIgnored = _parameters.exclude.shouldIgnore(node);
@@ -64,7 +67,33 @@ class AvoidReturningWidgetsVisitor extends RecursiveAstVisitor<void> {
6467
_rule.reportAtNode(node);
6568
}
6669

70+
bool _isStateWidgetCastingGetter(MethodDeclaration node) {
71+
final enclosingElement = node.declaredFragment?.element.enclosingElement;
72+
if (enclosingElement is! InterfaceElement ||
73+
!isWidgetStateOrSubclass(enclosingElement.thisType)) {
74+
return false;
75+
}
76+
77+
final unwrapped = node.singleReturnExpression.unwrapTarget;
78+
if (unwrapped?.targetExpression.isThisOrSuperOrNull != true) {
79+
return false;
80+
}
81+
82+
final element = unwrapped?.memberElement;
83+
final enclosing = element?.enclosingElement;
84+
85+
return element is PropertyAccessorElement &&
86+
element.name == 'widget' &&
87+
enclosing is InterfaceElement &&
88+
isWidgetStateOrSubclass(enclosing.thisType);
89+
}
90+
6791
bool _isOverridden(Declaration node) {
92+
if (node is MethodDeclaration &&
93+
node.metadata.any((m) => m.name.name == 'override')) {
94+
return true;
95+
}
96+
6897
return switch (node) {
6998
Declaration(
7099
declaredFragment: Fragment(

lib/src/utils/node_utils.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ extension ExpressionExtension on Expression {
273273
/// Returns the member element referenced or operated on by this expression,
274274
/// or null if none.
275275
Element? get memberElement => switch (this) {
276+
SimpleIdentifier(:final element) => element,
276277
MethodInvocation(:final methodName) => methodName.element,
277278
PropertyAccess(:final propertyName) => propertyName.element,
278279
AssignmentExpression(:final writeElement, :final readElement) ||
@@ -314,3 +315,17 @@ extension ExpressionNullableExtension on Expression? {
314315
/// Returns `true` if this expression is `this` or `super`.
315316
bool get isThisOrSuper => this is ThisExpression || this is SuperExpression;
316317
}
318+
319+
/// Extension on [MethodDeclaration] to provide AST helper getters.
320+
extension MethodDeclarationExtension on MethodDeclaration {
321+
/// Returns the single return expression of a method, or null if the
322+
/// method body has multiple statements or no return expression.
323+
Expression? get singleReturnExpression => switch (body) {
324+
ExpressionFunctionBody(:final expression) => expression,
325+
BlockFunctionBody(
326+
block: Block(statements: [ReturnStatement(:final expression?)]),
327+
) =>
328+
expression,
329+
_ => null,
330+
};
331+
}

lib/src/utils/types_utils.dart

Lines changed: 17 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import 'package:analyzer/dart/ast/ast.dart';
2626
import 'package:analyzer/dart/element/element.dart';
2727
import 'package:analyzer/dart/element/nullability_suffix.dart';
2828
import 'package:analyzer/dart/element/type.dart';
29-
import 'package:collection/collection.dart';
3029
import 'package:solid_lints/src/utils/named_type_utils.dart';
3130

3231
extension Subtypes on DartType {
@@ -144,16 +143,9 @@ extension InterfaceElementExt on InterfaceElement {
144143
}
145144
}
146145

147-
bool hasWidgetType(DartType type) =>
148-
(isWidgetOrSubclass(type) ||
149-
_isIterable(type) ||
150-
_isList(type) ||
151-
_isFuture(type)) &&
152-
!(_isMultiProvider(type) ||
153-
_isSubclassOfInheritedProvider(type) ||
154-
_isIterableInheritedProvider(type) ||
155-
_isListInheritedProvider(type) ||
156-
_isFutureInheritedProvider(type));
146+
bool isWidgetType(DartType type) =>
147+
isWidgetOrSubclass(type) &&
148+
!(_isMultiProvider(type) || _isSubclassOfInheritedProvider(type));
157149

158150
bool isIterable(DartType? type) =>
159151
_checkSelfOrSupertypes(type, (t) => t?.isDartCoreIterable ?? false);
@@ -205,52 +197,43 @@ bool _checkSelfOrSupertypes(
205197
predicate(type) ||
206198
(type is InterfaceType && type.allSupertypes.any(predicate));
207199

208-
bool _isWidget(DartType? type) => type?.getDisplayString() == 'Widget';
200+
bool _isWidget(DartType? type) => _isFlutterType(type, 'Widget');
209201

210202
bool _isSubclassOfWidget(DartType? type) =>
211203
type is InterfaceType && type.allSupertypes.any(_isWidget);
212204

213-
// ignore: deprecated_member_use
214-
bool _isWidgetState(DartType? type) => type?.element?.displayName == 'State';
205+
bool _isWidgetState(DartType? type) => _isFlutterType(type, 'State');
215206

216207
bool _isSubclassOfWidgetState(DartType? type) =>
217208
type is InterfaceType && type.allSupertypes.any(_isWidgetState);
218209

219-
bool _isIterable(DartType type) =>
220-
type.isDartCoreIterable &&
221-
type is InterfaceType &&
222-
isWidgetOrSubclass(type.typeArguments.firstOrNull);
223-
224-
bool _isList(DartType type) =>
225-
type.isDartCoreList &&
226-
type is InterfaceType &&
227-
isWidgetOrSubclass(type.typeArguments.firstOrNull);
228-
229-
bool _isFuture(DartType type) =>
230-
type.isDartAsyncFuture &&
231-
type is InterfaceType &&
232-
isWidgetOrSubclass(type.typeArguments.firstOrNull);
233-
234-
bool _isListenable(DartType type) => type.getDisplayString() == 'Listenable';
210+
bool _isListenable(DartType? type) => _isFlutterType(type, 'Listenable');
235211

236-
bool _isRenderObject(DartType? type) =>
237-
type?.getDisplayString() == 'RenderObject';
212+
bool _isRenderObject(DartType? type) => _isFlutterType(type, 'RenderObject');
238213

239214
bool _isSubclassOfRenderObject(DartType? type) =>
240215
type is InterfaceType && type.allSupertypes.any(_isRenderObject);
241216

242217
bool _isRenderObjectWidget(DartType? type) =>
243-
type?.getDisplayString() == 'RenderObjectWidget';
218+
_isFlutterType(type, 'RenderObjectWidget');
244219

245220
bool _isSubclassOfRenderObjectWidget(DartType? type) =>
246221
type is InterfaceType && type.allSupertypes.any(_isRenderObjectWidget);
247222

248223
bool _isRenderObjectElement(DartType? type) =>
249-
type?.getDisplayString() == 'RenderObjectElement';
224+
_isFlutterType(type, 'RenderObjectElement');
250225

251226
bool _isSubclassOfRenderObjectElement(DartType? type) =>
252227
type is InterfaceType && type.allSupertypes.any(_isRenderObjectElement);
253228

229+
bool _isFlutterType(DartType? type, String name) =>
230+
type is InterfaceType &&
231+
type.element.name == name &&
232+
_isFlutterLibrary(type.element.library);
233+
234+
bool _isFlutterLibrary(LibraryElement library) =>
235+
library.uri.scheme == 'package' && library.uri.path.startsWith('flutter/');
236+
254237
bool _isMultiProvider(DartType? type) =>
255238
type?.getDisplayString() == 'MultiProvider';
256239

@@ -260,21 +243,6 @@ bool _isSubclassOfInheritedProvider(DartType? type) =>
260243
bool _isInheritedProvider(DartType? type) =>
261244
type != null && type.getDisplayString().startsWith('InheritedProvider<');
262245

263-
bool _isIterableInheritedProvider(DartType type) =>
264-
type.isDartCoreIterable &&
265-
type is InterfaceType &&
266-
_isSubclassOfInheritedProvider(type.typeArguments.firstOrNull);
267-
268-
bool _isListInheritedProvider(DartType type) =>
269-
type.isDartCoreList &&
270-
type is InterfaceType &&
271-
_isSubclassOfInheritedProvider(type.typeArguments.firstOrNull);
272-
273-
bool _isFutureInheritedProvider(DartType type) =>
274-
type.isDartAsyncFuture &&
275-
type is InterfaceType &&
276-
_isSubclassOfInheritedProvider(type.typeArguments.firstOrNull);
277-
278246
bool isIterableOrSubclass(DartType? type) =>
279247
_checkSelfOrSupertypes(type, (t) => t?.isDartCoreIterable ?? false);
280248

test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ class BoxDecoration extends Widget {
6262
Widget build(BuildContext context) => throw 'unimplemented';
6363
}
6464
65+
abstract class State<T extends StatefulWidget> {
66+
T get widget => throw 'unimplemented';
67+
}
68+
69+
class Color {}
70+
71+
abstract interface class WidgetStateProperty<T> {}
72+
73+
class WidgetStateColor extends Color implements WidgetStateProperty<Color> {}
74+
6575
class DecoratedBox extends Widget {
6676
const DecoratedBox({required this.decoration});
6777
@@ -265,6 +275,112 @@ class NotExcludeWidget extends StatelessWidget {
265275
266276
${expectLint('Widget excludeWidgetMethod() => const SizedBox();')}
267277
}
278+
''');
279+
}
280+
281+
Future<void> test_does_not_report_on_collections() async {
282+
await assertNoDiagnostics('''
283+
$_importFlutterWidgets
284+
285+
class MyWidget extends StatelessWidget {
286+
const MyWidget({super.key});
287+
288+
List<Widget> buildList() => [const SizedBox()];
289+
290+
@override
291+
Widget build(BuildContext context) {
292+
return const SizedBox();
293+
}
294+
}
295+
''');
296+
}
297+
298+
Future<void> test_does_not_report_on_non_widget_types() async {
299+
await assertNoDiagnostics('''
300+
$_importFlutterWidgets
301+
302+
class MyWidget extends StatelessWidget {
303+
const MyWidget({super.key});
304+
305+
WidgetStateColor getColor() => WidgetStateColor();
306+
307+
@override
308+
Widget build(BuildContext context) {
309+
return const SizedBox();
310+
}
311+
}
312+
''');
313+
}
314+
315+
Future<void> test_does_not_report_on_abstract_methods() async {
316+
await assertNoDiagnostics('''
317+
$_importFlutterWidgets
318+
319+
abstract class BaseStrategy {
320+
Widget buildHeader(BuildContext context);
321+
}
322+
''');
323+
}
324+
325+
Future<void> test_does_not_report_on_state_widget_getters() async {
326+
await assertNoDiagnostics('''
327+
$_importFlutterWidgets
328+
329+
class TargetWidget extends StatefulWidget {
330+
const TargetWidget({super.key});
331+
}
332+
333+
class _TargetWidgetState extends State<StatefulWidget> {
334+
TargetWidget get widget => super.widget as TargetWidget;
335+
TargetWidget get parenthesizedWidget => ((super.widget as TargetWidget));
336+
TargetWidget get blockWidget {
337+
return super.widget as TargetWidget;
338+
}
339+
}
340+
''');
341+
}
342+
343+
Future<void> test_does_not_report_on_inline_builder_callbacks() async {
344+
await assertNoDiagnostics('''
345+
$_importFlutterWidgets
346+
347+
void acceptBuilder(Widget Function(BuildContext) builder) {}
348+
349+
class MyWidget extends StatelessWidget {
350+
const MyWidget({super.key});
351+
352+
@override
353+
Widget build(BuildContext context) {
354+
acceptBuilder((ctx) => const SizedBox());
355+
return const SizedBox();
356+
}
357+
}
358+
''');
359+
}
360+
361+
Future<void> test_reports_on_non_widget_state_accessors() async {
362+
await assertAutoDiagnostics('''
363+
$_importFlutterWidgets
364+
365+
class OtherState extends State<StatefulWidget> {
366+
${expectLint('Widget get someWidget => const SizedBox();')}
367+
}
368+
369+
class _TargetWidgetState extends State<StatefulWidget> {
370+
late final OtherState otherState;
371+
372+
${expectLint('Widget get customWidget => otherState.someWidget;')}
373+
}
374+
''');
375+
}
376+
377+
Future<void> test_does_not_report_on_local_non_flutter_widget_class() async {
378+
await assertNoDiagnostics('''
379+
class Widget {}
380+
381+
class CustomService {
382+
Widget createCustomWidget() => Widget();
383+
}
268384
''');
269385
}
270386
}

0 commit comments

Comments
 (0)