Skip to content

Commit b732a62

Browse files
bkonyiCommit Queue
authored andcommitted
[ analyzer ] Add support for "Wrap FutureBuilder" code assist
Fixes #34364 Change-Id: I53b005fdae61fe84ebc7defd54531df6dcd2a351 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/408341 Auto-Submit: Ben Konyi <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]> Commit-Queue: Brian Wilkerson <[email protected]>
1 parent 420ef2e commit b732a62

File tree

9 files changed

+245
-2
lines changed

9 files changed

+245
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
#### Analyzer
1414

1515
- Added the experimental [`unnecessary_ignore`][] lint rule.
16+
- Offer additional assist to wrap a Flutter widget with a `FutureBuilder` widget.
17+
1618

1719
[`unnecessary_ignore`]: http://dart.dev/lints/unnecessary_ignore
1820

pkg/analysis_server/lib/src/services/correction/assist.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,11 @@ abstract final class DartAssistKind {
301301
DartAssistKindPriority.FLUTTER_WRAP_SPECIFIC,
302302
'Wrap with Flexible',
303303
);
304+
static const FLUTTER_WRAP_FUTURE_BUILDER = AssistKind(
305+
'dart.assist.flutter.wrap.futureBuilder',
306+
DartAssistKindPriority.FLUTTER_WRAP_SPECIFIC,
307+
'Wrap with FutureBuilder',
308+
);
304309
static const FLUTTER_WRAP_PADDING = AssistKind(
305310
'dart.assist.flutter.wrap.padding',
306311
DartAssistKindPriority.FLUTTER_WRAP_SPECIFIC,

pkg/analysis_server/lib/src/services/correction/assist_internal.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import 'package:analysis_server/src/services/correction/dart/flutter_swap_with_c
5454
import 'package:analysis_server/src/services/correction/dart/flutter_swap_with_parent.dart';
5555
import 'package:analysis_server/src/services/correction/dart/flutter_wrap.dart';
5656
import 'package:analysis_server/src/services/correction/dart/flutter_wrap_builder.dart';
57+
import 'package:analysis_server/src/services/correction/dart/flutter_wrap_future_builder.dart';
5758
import 'package:analysis_server/src/services/correction/dart/flutter_wrap_generic.dart';
5859
import 'package:analysis_server/src/services/correction/dart/flutter_wrap_stream_builder.dart';
5960
import 'package:analysis_server/src/services/correction/dart/flutter_wrap_value_listenable_builder.dart';
@@ -142,6 +143,7 @@ class AssistProcessor {
142143
FlutterSwapWithChild.new,
143144
FlutterSwapWithParent.new,
144145
FlutterWrapBuilder.new,
146+
FlutterWrapFutureBuilder.new,
145147
FlutterWrapGeneric.new,
146148
FlutterWrapStreamBuilder.new,
147149
FlutterWrapValueListenableBuilder.new,

pkg/analysis_server/lib/src/services/correction/dart/flutter_wrap_builder.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import 'package:analyzer_plugin/utilities/assist/assist.dart';
1010
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
1111
import 'package:analyzer_plugin/utilities/range_factory.dart';
1212

13+
// TODO(bkonyi): share common implementation between the various builder wrappers
14+
// See https://github.com/dart-lang/sdk/issues/60075
1315
class FlutterWrapBuilder extends ResolvedCorrectionProducer {
1416
FlutterWrapBuilder({required super.context});
1517

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:analysis_server/src/services/correction/assist.dart';
6+
import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
7+
import 'package:analyzer/src/utilities/extensions/flutter.dart';
8+
import 'package:analyzer_plugin/utilities/assist/assist.dart';
9+
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
10+
import 'package:analyzer_plugin/utilities/range_factory.dart';
11+
12+
// TODO(bkonyi): share common implementation between the various builder wrappers
13+
// See https://github.com/dart-lang/sdk/issues/60075
14+
class FlutterWrapFutureBuilder extends ResolvedCorrectionProducer {
15+
FlutterWrapFutureBuilder({required super.context});
16+
17+
@override
18+
CorrectionApplicability get applicability =>
19+
// TODO(applicability): comment on why.
20+
CorrectionApplicability
21+
.singleLocation;
22+
23+
@override
24+
AssistKind get assistKind => DartAssistKind.FLUTTER_WRAP_FUTURE_BUILDER;
25+
26+
@override
27+
Future<void> compute(ChangeBuilder builder) async {
28+
var widgetExpr = node.findWidgetExpression;
29+
if (widgetExpr == null) {
30+
return;
31+
}
32+
var widgetSrc = utils.getNodeText(widgetExpr);
33+
34+
var FutureBuilderElement = await sessionHelper.getFlutterClass(
35+
'FutureBuilder',
36+
);
37+
if (FutureBuilderElement == null) {
38+
return;
39+
}
40+
41+
await builder.addDartFileEdit(file, (builder) {
42+
builder.addReplacement(range.node(widgetExpr), (builder) {
43+
builder.writeReference(FutureBuilderElement);
44+
45+
builder.write('<');
46+
builder.addSimpleLinkedEdit('type', 'Object');
47+
builder.writeln('>(');
48+
49+
var indentOld = utils.getLinePrefix(widgetExpr.offset);
50+
var indentNew1 = indentOld + utils.oneIndent;
51+
var indentNew2 = indentOld + utils.twoIndents;
52+
53+
builder.write(indentNew1);
54+
builder.writeln('future: null,');
55+
56+
builder.write(indentNew1);
57+
builder.writeln('builder: (context, snapshot) {');
58+
59+
widgetSrc = utils.replaceSourceIndent(widgetSrc, indentOld, indentNew2);
60+
builder.write(indentNew2);
61+
builder.write('return $widgetSrc');
62+
builder.writeln(';');
63+
64+
builder.write(indentNew1);
65+
var addTrailingCommas =
66+
getCodeStyleOptions(unitResult.file).addTrailingCommas;
67+
builder.writeln('}${addTrailingCommas ? "," : ""}');
68+
69+
builder.write(indentOld);
70+
builder.write(')');
71+
});
72+
});
73+
}
74+
}

pkg/analysis_server/lib/src/services/correction/dart/flutter_wrap_stream_builder.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import 'package:analyzer_plugin/utilities/assist/assist.dart';
1010
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
1111
import 'package:analyzer_plugin/utilities/range_factory.dart';
1212

13+
// TODO(bkonyi): share common implementation between the various builder wrappers
14+
// See https://github.com/dart-lang/sdk/issues/60075
1315
class FlutterWrapStreamBuilder extends ResolvedCorrectionProducer {
1416
FlutterWrapStreamBuilder({required super.context});
1517

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:analysis_server/src/services/correction/assist.dart';
6+
import 'package:analyzer_plugin/utilities/assist/assist.dart';
7+
import 'package:linter/src/lint_names.dart';
8+
import 'package:test_reflective_loader/test_reflective_loader.dart';
9+
10+
import 'assist_processor.dart';
11+
12+
void main() {
13+
defineReflectiveSuite(() {
14+
defineReflectiveTests(FlutterWrapFutureBuilderTest);
15+
});
16+
}
17+
18+
@reflectiveTest
19+
class FlutterWrapFutureBuilderTest extends AssistProcessorTest {
20+
@override
21+
AssistKind get kind => DartAssistKind.FLUTTER_WRAP_FUTURE_BUILDER;
22+
23+
@override
24+
void setUp() {
25+
super.setUp();
26+
writeTestPackageConfig(flutter: true);
27+
}
28+
29+
Future<void> test_aroundFutureBuilder() async {
30+
await resolveTestCode('''
31+
import 'package:flutter/widgets.dart';
32+
33+
void f(Future<int> s) {
34+
/*caret*/FutureBuilder(
35+
future: s,
36+
builder: (context, snapshot) => Text(''),
37+
);
38+
}
39+
''');
40+
await assertHasAssist('''
41+
import 'package:flutter/widgets.dart';
42+
43+
void f(Future<int> s) {
44+
FutureBuilder<Object>(
45+
future: null,
46+
builder: (context, snapshot) {
47+
return FutureBuilder(
48+
future: s,
49+
builder: (context, snapshot) => Text(''),
50+
);
51+
}
52+
);
53+
}
54+
''');
55+
}
56+
57+
Future<void> test_aroundText() async {
58+
await resolveTestCode('''
59+
import 'package:flutter/widgets.dart';
60+
61+
void f() {
62+
/*caret*/Text('a');
63+
}
64+
''');
65+
await assertHasAssist('''
66+
import 'package:flutter/widgets.dart';
67+
68+
void f() {
69+
FutureBuilder<Object>(
70+
future: null,
71+
builder: (context, snapshot) {
72+
return Text('a');
73+
}
74+
);
75+
}
76+
''');
77+
}
78+
79+
Future<void> test_trailingComma_disabled() async {
80+
// No analysis options.
81+
await resolveTestCode('''
82+
import 'package:flutter/widgets.dart';
83+
84+
class TestWidget extends StatelessWidget {
85+
const TestWidget({super.key});
86+
@override
87+
Widget build(BuildContext context) {
88+
return const /*caret*/Text('hi');
89+
}
90+
}
91+
''');
92+
await assertHasAssist('''
93+
import 'package:flutter/widgets.dart';
94+
95+
class TestWidget extends StatelessWidget {
96+
const TestWidget({super.key});
97+
@override
98+
Widget build(BuildContext context) {
99+
return FutureBuilder<Object>(
100+
future: null,
101+
builder: (context, snapshot) {
102+
return const Text('hi');
103+
}
104+
);
105+
}
106+
}
107+
''');
108+
}
109+
110+
Future<void> test_trailingComma_enabled() async {
111+
createAnalysisOptionsFile(lints: [LintNames.require_trailing_commas]);
112+
await resolveTestCode('''
113+
import 'package:flutter/widgets.dart';
114+
115+
class TestWidget extends StatelessWidget {
116+
const TestWidget({super.key});
117+
@override
118+
Widget build(BuildContext context) {
119+
return const /*caret*/Text('hi');
120+
}
121+
}
122+
''');
123+
await assertHasAssist('''
124+
import 'package:flutter/widgets.dart';
125+
126+
class TestWidget extends StatelessWidget {
127+
const TestWidget({super.key});
128+
@override
129+
Widget build(BuildContext context) {
130+
return FutureBuilder<Object>(
131+
future: null,
132+
builder: (context, snapshot) {
133+
return const Text('hi');
134+
},
135+
);
136+
}
137+
}
138+
''');
139+
}
140+
}

pkg/analysis_server/test/src/services/correction/assist/test_all.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import 'flutter_wrap_column_test.dart' as flutter_wrap_column;
6868
import 'flutter_wrap_container_test.dart' as flutter_wrap_container;
6969
import 'flutter_wrap_expanded_test.dart' as flutter_wrap_expanded;
7070
import 'flutter_wrap_flexible_test.dart' as flutter_wrap_flexible;
71+
import 'flutter_wrap_future_builder_test.dart' as flutter_wrap_future_builder;
7172
import 'flutter_wrap_generic_test.dart' as flutter_wrap_generic;
7273
import 'flutter_wrap_padding_test.dart' as flutter_wrap_padding;
7374
import 'flutter_wrap_row_test.dart' as flutter_wrap_row;
@@ -160,6 +161,7 @@ void main() {
160161
flutter_wrap_container.main();
161162
flutter_wrap_expanded.main();
162163
flutter_wrap_flexible.main();
164+
flutter_wrap_future_builder.main();
163165
flutter_wrap_generic.main();
164166
flutter_wrap_padding.main();
165167
flutter_wrap_row.main();

pkg/analyzer_utilities/lib/test/mock_packages/package_content/flutter/lib/src/widgets/async.dart

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,24 @@ typedef AsyncWidgetBuilder<T> = Widget Function(
99

1010
class AsyncSnapshot<T> {}
1111

12-
class StreamBuilder<T> {
12+
class StreamBuilder<T> extends StatefulWidget {
1313
final T? initialData;
1414
final AsyncWidgetBuilder<T> builder;
1515

1616
const StreamBuilder(
17-
{Key? key, this.initialData, Stream<T>? stream, @required this.builder});
17+
{Key? key,
18+
this.initialData,
19+
required Stream<T>? stream,
20+
required this.builder});
21+
}
22+
23+
class FutureBuilder<T> extends StatefulWidget {
24+
final T? initialData;
25+
final AsyncWidgetBuilder<T> builder;
26+
27+
const FutureBuilder(
28+
{Key? key,
29+
this.initialData,
30+
required Future<T>? future,
31+
required this.builder});
1832
}

0 commit comments

Comments
 (0)