Skip to content

Commit dc37a77

Browse files
nshahanCommit Queue
authored and
Commit Queue
committed
[ddc] Add rejection for removed const fields
Reject hot reload requests that remove fields from const classes. This is consistent with the VM implementation. Change-Id: I73ff62795abbca62565ee6efd9cb245485d010ac Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/403321 Reviewed-by: Morgan :) <[email protected]> Commit-Queue: Nicholas Shahan <[email protected]> Reviewed-by: Nate Biggs <[email protected]>
1 parent 9a47293 commit dc37a77

File tree

3 files changed

+142
-3
lines changed

3 files changed

+142
-3
lines changed

pkg/dev_compiler/lib/src/kernel/hot_reload_delta_inspector.dart

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ class HotReloadDeltaInspector {
3434
// No previous version of the class to compare with.
3535
continue;
3636
}
37-
_checkConstClassConsistency(acceptedClass, deltaClass);
37+
if (acceptedClass.hasConstConstructor) {
38+
_checkConstClassConsistency(acceptedClass, deltaClass);
39+
_checkConstClassDeletedFields(acceptedClass, deltaClass);
40+
}
3841
}
3942
}
4043
return _rejectionMessages;
@@ -46,10 +49,36 @@ class HotReloadDeltaInspector {
4649
/// [acceptedClass] and [deltaClass] must represent the same class in the
4750
/// last known accepted and delta components respectively.
4851
void _checkConstClassConsistency(Class acceptedClass, Class deltaClass) {
49-
if (acceptedClass.hasConstConstructor && !deltaClass.hasConstConstructor) {
52+
assert(acceptedClass.hasConstConstructor);
53+
if (!deltaClass.hasConstConstructor) {
5054
_rejectionMessages.add('Const class cannot become non-const: '
5155
"Library:'${deltaClass.enclosingLibrary.importUri}' "
5256
'Class: ${deltaClass.name}');
5357
}
5458
}
59+
60+
/// Records a rejection error when [acceptedClass] and [deltaClass] are both
61+
/// const but fields have been removed from [deltaClass].
62+
///
63+
/// [acceptedClass] and [deltaClass] must represent the same class in the
64+
/// last known accepted and delta components respectively.
65+
void _checkConstClassDeletedFields(Class acceptedClass, Class deltaClass) {
66+
assert(acceptedClass.hasConstConstructor);
67+
if (!deltaClass.hasConstConstructor) {
68+
// Avoid reporting errors when fields are removed but the delta class is
69+
// also no longer const. That is already reported by
70+
// [_checkConstClassConsistency].
71+
return;
72+
}
73+
// Verify all fields are still present.
74+
final acceptedFields = {
75+
for (var field in acceptedClass.fields) field.name.text
76+
};
77+
final deltaFields = {for (var field in deltaClass.fields) field.name.text};
78+
if (acceptedFields.difference(deltaFields).isNotEmpty) {
79+
_rejectionMessages.add('Const class cannot remove fields: '
80+
"Library:'${deltaClass.enclosingLibrary.importUri}' "
81+
'Class: ${deltaClass.name}');
82+
}
83+
}
5584
}

pkg/dev_compiler/test/hot_reload_delta_inspector_test.dart

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,108 @@ Future<void> main() async {
155155
await compileComponents(initialSource, deltaSource);
156156
expect(deltaInspector.compareGenerations(initial, delta), isEmpty);
157157
});
158+
test('rejection when removing a field', () async {
159+
final initialSource = '''
160+
var globalVariable;
161+
162+
class A {
163+
final String s, t, w;
164+
const A(this.s, this.t, this.w);
165+
}
166+
167+
main() {
168+
globalVariable = const A('hello', 'world', '!');
169+
print(globalVariable.s);
170+
}
171+
''';
172+
final deltaSource = '''
173+
var globalVariable;
174+
175+
class A {
176+
final String s, t;
177+
const A(this.s, this.t);
178+
}
179+
180+
main() {
181+
print('hello world');
182+
}
183+
''';
184+
final (:initial, :delta) =
185+
await compileComponents(initialSource, deltaSource);
186+
expect(
187+
deltaInspector.compareGenerations(initial, delta),
188+
unorderedEquals([
189+
'Const class cannot remove fields: '
190+
"Library:'memory:///main.dart' Class: A"
191+
]));
192+
});
193+
test('rejection when removing a field while adding another', () async {
194+
final initialSource = '''
195+
var globalVariable;
196+
197+
class A {
198+
final String s, t, w;
199+
const A(this.s, this.t, this.w);
200+
}
201+
202+
main() {
203+
globalVariable = const A('hello', 'world', '!');
204+
print(globalVariable.s);
205+
}
206+
''';
207+
final deltaSource = '''
208+
var globalVariable;
209+
210+
class A {
211+
final String s, t, x;
212+
const A(this.s, this.t, this.x);
213+
}
214+
215+
main() {
216+
print('hello world');
217+
}
218+
''';
219+
final (:initial, :delta) =
220+
await compileComponents(initialSource, deltaSource);
221+
expect(
222+
deltaInspector.compareGenerations(initial, delta),
223+
unorderedEquals([
224+
'Const class cannot remove fields: '
225+
"Library:'memory:///main.dart' Class: A"
226+
]));
227+
});
228+
test('no error when removing field while also making class const',
229+
() async {
230+
final initialSource = '''
231+
var globalVariable;
232+
233+
class A {
234+
final String s, t, w;
235+
A(this.s, this.t, this.w);
236+
}
237+
238+
main() {
239+
globalVariable = A('hello', 'world', '!');
240+
print(globalVariable.s);
241+
}
242+
''';
243+
final deltaSource = '''
244+
var globalVariable;
245+
246+
class A {
247+
final String s, t;
248+
const A(this.s, this.t);
249+
}
250+
251+
main() {
252+
print('hello world');
253+
}
254+
''';
255+
final (:initial, :delta) =
256+
await compileComponents(initialSource, deltaSource);
257+
expect(() => deltaInspector.compareGenerations(initial, delta),
258+
returnsNormally);
259+
});
158260
});
159261
}
160262

pkg/reload_test/lib/frontend_server_controller.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,8 @@ class HotReloadFrontendServerController {
202202

203203
/// Clears the controller's state between commands.
204204
///
205-
/// Note: this does not reset the Frontend Server's state.
205+
/// Note: this does not reset the Frontend Server's state and for that reason
206+
/// the local [totalErrors] count is not reset here.
206207
void _clearState() {
207208
sources.clear();
208209
accumulatedOutput.clear();
@@ -227,6 +228,13 @@ class HotReloadFrontendServerController {
227228
Future<CompilerOutput> sendRecompile(String entrypointPath,
228229
{List<String> invalidatedFiles = const [],
229230
String boundaryKey = fakeBoundaryKey}) async {
231+
// Currently the `FrontendCompiler` used in this test suite clears errors
232+
// before performing a recompile but not an initial compile. Since we reuse
233+
// the same instance and issue an initial compile request for each test we
234+
// must track the number of errors on our side carefully so we can tell when
235+
// a new error has appeared. Resetting the [totalErrors] count here should
236+
// match the state on the FES side.
237+
totalErrors = 0;
230238
if (!started) throw Exception('Frontend Server has not been started yet.');
231239
_state = FrontendServerState.awaitingResult;
232240
final command = 'recompile $entrypointPath $boundaryKey\n'

0 commit comments

Comments
 (0)