[ddc] Add hot reload checks in aysnc bodies
Treats async code like function expressions when adding extra checks for correctness after a hot reload. This is intended to handle the cases where code that was statically sound and enqueued before the reload actually runs after a reload when the soundness may no longer be valid. Change-Id: I42cd010175126748937d7df830875bc5b850a565 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/444368 Reviewed-by: Nate Biggs <natebiggs@google.com> Commit-Queue: Nicholas Shahan <nshahan@google.com>
This commit is contained in:
committed by
Commit Queue
parent
5199e1493e
commit
cff9ab2c8e
@@ -873,6 +873,7 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
|
||||
final FutureOrNormalizer _futureOrNormalizer;
|
||||
|
||||
bool _inFunctionExpression = false;
|
||||
bool _inAsyncExpression = false;
|
||||
|
||||
/// Returns whether or not [uri] can be hot reloaded.
|
||||
///
|
||||
@@ -5072,8 +5073,14 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
|
||||
}
|
||||
|
||||
js_ast.Statement _emitFunctionScopedBody(FunctionNode f) {
|
||||
var savedInAsyncExpression = _inAsyncExpression;
|
||||
if (f.asyncMarker != AsyncMarker.Sync) {
|
||||
_inAsyncExpression = true;
|
||||
}
|
||||
var jsBody = _visitStatement(f.body!);
|
||||
return _emitScopedBody(f, jsBody);
|
||||
var body = _emitScopedBody(f, jsBody);
|
||||
_inAsyncExpression = savedInAsyncExpression;
|
||||
return body;
|
||||
}
|
||||
|
||||
js_ast.Statement _emitScopedBody(FunctionNode f, js_ast.Statement body) {
|
||||
@@ -7732,7 +7739,7 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
|
||||
/// include checks to preserve soundness in the presence of hot reloads at
|
||||
/// runtime.
|
||||
bool _shouldRewriteInvocationWithHotReloadChecks(Member target) =>
|
||||
_inFunctionExpression &&
|
||||
(_inFunctionExpression || _inAsyncExpression) &&
|
||||
!_isBuildingSdk &&
|
||||
!usesJSInterop(target) &&
|
||||
target != _assertInteropMethod;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"exclude": [
|
||||
"vm"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:expect/async_helper.dart';
|
||||
import 'package:reload_test/reload_test_utils.dart';
|
||||
|
||||
/// Shows the behavior when async code enqueued before a hot restart runs
|
||||
/// after the restart and tries to interact with code that has been deleted.
|
||||
|
||||
bool setBeforeAwait = false;
|
||||
bool setAfterAwait = false;
|
||||
|
||||
int deleted() {
|
||||
return 10;
|
||||
}
|
||||
|
||||
Future<int> helper(Future<void> timingControl) async {
|
||||
setBeforeAwait = true;
|
||||
await timingControl;
|
||||
setAfterAwait = true;
|
||||
return deleted();
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
var helperCompleter = Completer<void>();
|
||||
var helperFuture = helper(helperCompleter.future);
|
||||
Expect.isTrue(setBeforeAwait);
|
||||
Expect.isFalse(setAfterAwait);
|
||||
await hotReload();
|
||||
Expect.isFalse(setAfterAwait);
|
||||
helperCompleter.complete();
|
||||
var e = await asyncExpectThrows<NoSuchMethodError>(helperFuture);
|
||||
Expect.contains(
|
||||
"NoSuchMethodError: 'deleted'\n"
|
||||
'Method was deleted during a hot reload and is no longer callable.',
|
||||
e.toString(),
|
||||
);
|
||||
Expect.isTrue(setAfterAwait);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:expect/async_helper.dart';
|
||||
import 'package:reload_test/reload_test_utils.dart';
|
||||
|
||||
/// Shows the behavior when async code enqueued before a hot restart runs
|
||||
/// after the restart and tries to interact with code that has been deleted.
|
||||
|
||||
bool setBeforeAwait = false;
|
||||
bool setAfterAwait = false;
|
||||
|
||||
Future<int> helper(Future<void> timingControl) async {
|
||||
setBeforeAwait = true;
|
||||
await timingControl;
|
||||
setAfterAwait = true;
|
||||
return 99;
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
var helperCompleter = Completer<void>();
|
||||
var helperFuture = helper(helperCompleter.future);
|
||||
Expect.isTrue(setBeforeAwait);
|
||||
Expect.isFalse(setAfterAwait);
|
||||
await hotReload();
|
||||
Expect.isFalse(setAfterAwait);
|
||||
helperCompleter.complete();
|
||||
var e = await asyncExpectThrows<NoSuchMethodError>(helperFuture);
|
||||
Expect.contains(
|
||||
"NoSuchMethodError: 'deleted'\n"
|
||||
'Method was deleted during a hot reload and is no longer callable.',
|
||||
e.toString(),
|
||||
);
|
||||
Expect.isTrue(setAfterAwait);
|
||||
}
|
||||
|
||||
/** DIFF **/
|
||||
/*
|
||||
bool setBeforeAwait = false;
|
||||
bool setAfterAwait = false;
|
||||
|
||||
-int deleted() {
|
||||
- return 10;
|
||||
-}
|
||||
-
|
||||
Future<int> helper(Future<void> timingControl) async {
|
||||
setBeforeAwait = true;
|
||||
await timingControl;
|
||||
setAfterAwait = true;
|
||||
- return deleted();
|
||||
+ return 99;
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
*/
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"exclude": [
|
||||
"chrome",
|
||||
"d8"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:reload_test/reload_test_utils.dart';
|
||||
|
||||
/// Shows the behavior when async code enqueued before a hot restart runs
|
||||
/// after the restart and tries to interact with code that has been deleted.
|
||||
|
||||
bool setBeforeAwait = false;
|
||||
bool setAfterAwait = false;
|
||||
|
||||
int deleted() {
|
||||
return 10;
|
||||
}
|
||||
|
||||
Future<int> helper(Future<void> timingControl) async {
|
||||
setBeforeAwait = true;
|
||||
await timingControl;
|
||||
setAfterAwait = true;
|
||||
return deleted();
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
var helperCompleter = Completer<void>();
|
||||
var helperFuture = helper(helperCompleter.future);
|
||||
Expect.isTrue(setBeforeAwait);
|
||||
Expect.isFalse(setAfterAwait);
|
||||
await hotReload();
|
||||
Expect.isFalse(setAfterAwait);
|
||||
helperCompleter.complete();
|
||||
Expect.equals(10, await helperFuture);
|
||||
Expect.isTrue(setAfterAwait);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:reload_test/reload_test_utils.dart';
|
||||
|
||||
/// Shows the behavior when async code enqueued before a hot restart runs
|
||||
/// after the restart and tries to interact with code that has been deleted.
|
||||
|
||||
bool setBeforeAwait = false;
|
||||
bool setAfterAwait = false;
|
||||
|
||||
Future<int> helper(Future<void> timingControl) async {
|
||||
setBeforeAwait = true;
|
||||
await timingControl;
|
||||
setAfterAwait = true;
|
||||
return 99;
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
var helperCompleter = Completer<void>();
|
||||
var helperFuture = helper(helperCompleter.future);
|
||||
Expect.isTrue(setBeforeAwait);
|
||||
Expect.isFalse(setAfterAwait);
|
||||
await hotReload();
|
||||
Expect.isFalse(setAfterAwait);
|
||||
helperCompleter.complete();
|
||||
Expect.equals(10, await helperFuture);
|
||||
Expect.isTrue(setAfterAwait);
|
||||
}
|
||||
|
||||
/** DIFF **/
|
||||
/*
|
||||
bool setBeforeAwait = false;
|
||||
bool setAfterAwait = false;
|
||||
|
||||
-int deleted() {
|
||||
- return 10;
|
||||
-}
|
||||
-
|
||||
Future<int> helper(Future<void> timingControl) async {
|
||||
setBeforeAwait = true;
|
||||
await timingControl;
|
||||
setAfterAwait = true;
|
||||
- return deleted();
|
||||
+ return 99;
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
*/
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"exclude": [
|
||||
"vm"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:expect/async_helper.dart';
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:reload_test/reload_test_utils.dart';
|
||||
|
||||
/// Shows the behavior when async code enqueued before a hot restart runs
|
||||
/// after the restart and tries to interact with code that has been updated.
|
||||
|
||||
bool setBeforeAwait = false;
|
||||
bool setAfterAwait = false;
|
||||
|
||||
int changed(String s) {
|
||||
return s.length;
|
||||
}
|
||||
|
||||
Future<int> helper(Future<void> timingControl) async {
|
||||
setBeforeAwait = true;
|
||||
await timingControl;
|
||||
setAfterAwait = true;
|
||||
return changed('hello');
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
var helperCompleter = Completer<void>();
|
||||
var helperFuture = helper(helperCompleter.future);
|
||||
Expect.isTrue(setBeforeAwait);
|
||||
Expect.isFalse(setAfterAwait);
|
||||
await hotReload();
|
||||
Expect.isFalse(setAfterAwait);
|
||||
helperCompleter.complete();
|
||||
var error = await asyncExpectThrows<TypeError>(helperFuture);
|
||||
Expect.contains(
|
||||
"type 'String' is not a subtype of type 'int'",
|
||||
error.toString(),
|
||||
);
|
||||
Expect.isTrue(setAfterAwait);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:expect/async_helper.dart';
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:reload_test/reload_test_utils.dart';
|
||||
|
||||
/// Shows the behavior when async code enqueued before a hot restart runs
|
||||
/// after the restart and tries to interact with code that has been updated.
|
||||
|
||||
bool setBeforeAwait = false;
|
||||
bool setAfterAwait = false;
|
||||
|
||||
int changed(int i) {
|
||||
return i - 10;
|
||||
}
|
||||
|
||||
Future<int> helper(Future<void> timingControl) async {
|
||||
setBeforeAwait = true;
|
||||
await timingControl;
|
||||
setAfterAwait = true;
|
||||
return changed(99);
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
var helperCompleter = Completer<void>();
|
||||
var helperFuture = helper(helperCompleter.future);
|
||||
Expect.isTrue(setBeforeAwait);
|
||||
Expect.isFalse(setAfterAwait);
|
||||
await hotReload();
|
||||
Expect.isFalse(setAfterAwait);
|
||||
helperCompleter.complete();
|
||||
var error = await asyncExpectThrows<TypeError>(helperFuture);
|
||||
Expect.contains(
|
||||
"type 'String' is not a subtype of type 'int'",
|
||||
error.toString(),
|
||||
);
|
||||
Expect.isTrue(setAfterAwait);
|
||||
}
|
||||
|
||||
/** DIFF **/
|
||||
/*
|
||||
bool setBeforeAwait = false;
|
||||
bool setAfterAwait = false;
|
||||
|
||||
-int changed(String s) {
|
||||
- return s.length;
|
||||
+int changed(int i) {
|
||||
+ return i - 10;
|
||||
}
|
||||
|
||||
Future<int> helper(Future<void> timingControl) async {
|
||||
setBeforeAwait = true;
|
||||
await timingControl;
|
||||
setAfterAwait = true;
|
||||
- return changed('hello');
|
||||
+ return changed(99);
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
*/
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"exclude": [
|
||||
"chrome",
|
||||
"d8"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:expect/async_helper.dart';
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:reload_test/reload_test_utils.dart';
|
||||
|
||||
/// Shows the behavior when async code enqueued before a hot restart runs
|
||||
/// after the restart and tries to interact with code that has been updated.
|
||||
|
||||
bool setBeforeAwait = false;
|
||||
bool setAfterAwait = false;
|
||||
|
||||
int changed(String s) {
|
||||
return s.length;
|
||||
}
|
||||
|
||||
Future<int> helper(Future<void> timingControl) async {
|
||||
setBeforeAwait = true;
|
||||
await timingControl;
|
||||
setAfterAwait = true;
|
||||
return changed('hello');
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
var helperCompleter = Completer<void>();
|
||||
var helperFuture = helper(helperCompleter.future);
|
||||
Expect.isTrue(setBeforeAwait);
|
||||
Expect.isFalse(setAfterAwait);
|
||||
await hotReload();
|
||||
Expect.isFalse(setAfterAwait);
|
||||
helperCompleter.complete();
|
||||
var error = await asyncExpectThrows<NoSuchMethodError>(helperFuture);
|
||||
Expect.contains(
|
||||
"Class 'String' has no instance method '-'",
|
||||
error.toString(),
|
||||
);
|
||||
Expect.isTrue(setAfterAwait);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:expect/async_helper.dart';
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:reload_test/reload_test_utils.dart';
|
||||
|
||||
/// Shows the behavior when async code enqueued before a hot restart runs
|
||||
/// after the restart and tries to interact with code that has been updated.
|
||||
|
||||
bool setBeforeAwait = false;
|
||||
bool setAfterAwait = false;
|
||||
|
||||
int changed(int i) {
|
||||
return i - 10;
|
||||
}
|
||||
|
||||
Future<int> helper(Future<void> timingControl) async {
|
||||
setBeforeAwait = true;
|
||||
await timingControl;
|
||||
setAfterAwait = true;
|
||||
return changed(99);
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
var helperCompleter = Completer<void>();
|
||||
var helperFuture = helper(helperCompleter.future);
|
||||
Expect.isTrue(setBeforeAwait);
|
||||
Expect.isFalse(setAfterAwait);
|
||||
await hotReload();
|
||||
Expect.isFalse(setAfterAwait);
|
||||
helperCompleter.complete();
|
||||
var error = await asyncExpectThrows<NoSuchMethodError>(helperFuture);
|
||||
Expect.contains(
|
||||
"Class 'String' has no instance method '-'",
|
||||
error.toString(),
|
||||
);
|
||||
Expect.isTrue(setAfterAwait);
|
||||
}
|
||||
|
||||
/** DIFF **/
|
||||
/*
|
||||
bool setBeforeAwait = false;
|
||||
bool setAfterAwait = false;
|
||||
|
||||
-int changed(String s) {
|
||||
- return s.length;
|
||||
+int changed(int i) {
|
||||
+ return i - 10;
|
||||
}
|
||||
|
||||
Future<int> helper(Future<void> timingControl) async {
|
||||
setBeforeAwait = true;
|
||||
await timingControl;
|
||||
setAfterAwait = true;
|
||||
- return changed('hello');
|
||||
+ return changed(99);
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
*/
|
||||
@@ -13,11 +13,13 @@ class Foo {
|
||||
int x = 42;
|
||||
}
|
||||
|
||||
helper() => Foo().x;
|
||||
|
||||
Future<void> main() async {
|
||||
Expect.type<int>(Foo().x);
|
||||
Expect.equals(42, Foo().x);
|
||||
Expect.type<int>(helper());
|
||||
Expect.equals(42, helper());
|
||||
await hotReload();
|
||||
|
||||
Expect.type<String>(Foo().x);
|
||||
Expect.equals('42', Foo().x);
|
||||
Expect.type<String>(helper());
|
||||
Expect.equals('42', helper());
|
||||
}
|
||||
|
||||
@@ -13,13 +13,15 @@ class Foo {
|
||||
String x = '42';
|
||||
}
|
||||
|
||||
helper() => Foo().x;
|
||||
|
||||
Future<void> main() async {
|
||||
Expect.type<int>(Foo().x);
|
||||
Expect.equals(42, Foo().x);
|
||||
Expect.type<int>(helper());
|
||||
Expect.equals(42, helper());
|
||||
await hotReload();
|
||||
|
||||
Expect.type<String>(Foo().x);
|
||||
Expect.equals('42', Foo().x);
|
||||
Expect.type<String>(helper());
|
||||
Expect.equals('42', helper());
|
||||
}
|
||||
|
||||
/** DIFF **/
|
||||
@@ -31,5 +33,5 @@ Future<void> main() async {
|
||||
+ String x = '42';
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
helper() => Foo().x;
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user