Skip to content

Commit 113df82

Browse files
nshahanCommit Queue
authored andcommitted
[ddc] Add validation to instance setter calls
Ensures soundness for instance setter calls after a hot reload. Issue: #60100 Issue: #60112 Change-Id: Ie6736d002752076e5526bd37ae9f103040ed8cdd Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/437124 Reviewed-by: Mark Zhou <[email protected]> Commit-Queue: Nicholas Shahan <[email protected]>
1 parent bcaf426 commit 113df82

File tree

14 files changed

+363
-23
lines changed

14 files changed

+363
-23
lines changed

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

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6171,6 +6171,8 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
61716171
);
61726172
var instanceGet = js_ast.PropertyAccess(jsReceiver, jsMemberName);
61736173
if (_shouldRewriteInvocationWithHotReloadChecks(node.interfaceTarget)) {
6174+
// TODO(nshahan): Add a runtime helper to perform the checks only so the
6175+
// call site can be monomorphic.
61746176
instanceGet.sourceInformation = _nodeStart(node);
61756177
// Since there are no arguments (unlike methods) the dynamic get path can
61766178
// be reused for the hot reload checks on a getter.
@@ -6245,23 +6247,43 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
62456247
member.name.text == 'call' && isNonStaticJsInterop(member);
62466248

62476249
@override
6248-
js_ast.Expression visitDynamicSet(DynamicSet node) {
6249-
return _runtimeCall('dput$_replSuffix(#, #, #)', [
6250-
_visitExpression(node.receiver),
6251-
_emitMemberName(node.name.text),
6252-
_visitExpression(node.value),
6253-
]);
6254-
}
6250+
js_ast.Expression visitDynamicSet(DynamicSet node) => _emitDynamicSet(
6251+
_visitExpression(node.receiver),
6252+
_emitMemberName(node.name.text),
6253+
_visitExpression(node.value),
6254+
);
6255+
6256+
js_ast.Expression _emitDynamicSet(
6257+
js_ast.Expression receiver,
6258+
js_ast.Expression memberName,
6259+
js_ast.Expression value,
6260+
) => _runtimeCall('dput$_replSuffix(#, #, #)', [receiver, memberName, value]);
62556261

62566262
@override
62576263
js_ast.Expression visitInstanceSet(InstanceSet node) {
62586264
var target = node.interfaceTarget;
6259-
var value = isJsMember(target) ? _assertInterop(node.value) : node.value;
6260-
return js.call('#.# = #', [
6261-
_visitExpression(node.receiver),
6262-
_emitMemberName(node.name.text, member: target),
6263-
_visitExpression(value),
6264-
]);
6265+
var receiver = _visitExpression(node.receiver);
6266+
var memberName = _emitMemberName(node.name.text, member: target);
6267+
var value = _visitExpression(
6268+
isJsMember(target) ? _assertInterop(node.value) : node.value,
6269+
);
6270+
var uncheckedSet = js.call('#.# = #', [receiver, memberName, value]);
6271+
if (_shouldRewriteInvocationWithHotReloadChecks(target)) {
6272+
// TODO(nshahan): Add a runtime helper to perform the checks only so the
6273+
// call site can be monomorphic.
6274+
var checkedSet = _emitDynamicSet(receiver, memberName, value);
6275+
// Dart setters can contain a return statement but that value doesn't get
6276+
// returned anywhere. Instead, the result of an assignment expression is
6277+
// the RHS. DDC relies on the same behavior in the JavaScript
6278+
// representation to carry the value through as the result of the
6279+
// assignment expression. It is then sufficient to check the type of the
6280+
// argument as it flows into the setter. If it is still valid, then it
6281+
// should also be valid to flow through to the context where the
6282+
// assignment appears. No extra cast on the assignment expression is
6283+
// needed.
6284+
return _emitHotReloadSafeInvocation(uncheckedSet, checkedSet);
6285+
}
6286+
return uncheckedSet;
62656287
}
62666288

62676289
/// True when the result of evaluating [e] is not known to have the Object
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"exclude": [
3+
"vm"
4+
]
5+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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:expect/expect.dart';
6+
import 'package:reload_test/reload_test_utils.dart';
7+
8+
var retained;
9+
C? c;
10+
11+
class C {
12+
set deleted(String s) {}
13+
}
14+
15+
helper() {
16+
c = C();
17+
retained = () => c!.deleted = 'hello';
18+
return retained();
19+
}
20+
21+
Future<void> main() async {
22+
helper();
23+
await hotReload();
24+
Expect.throws<NoSuchMethodError>(
25+
helper,
26+
(error) => '$error'.contains('deleted'),
27+
);
28+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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:expect/expect.dart';
6+
import 'package:reload_test/reload_test_utils.dart';
7+
8+
var retained;
9+
C? c;
10+
11+
class C {}
12+
13+
helper() {
14+
return retained();
15+
}
16+
17+
Future<void> main() async {
18+
helper();
19+
await hotReload();
20+
Expect.throws<NoSuchMethodError>(
21+
helper,
22+
(error) => '$error'.contains('deleted'),
23+
);
24+
}
25+
26+
/** DIFF **/
27+
/*
28+
var retained;
29+
C? c;
30+
31+
-class C {
32+
- set deleted(String s) {}
33+
-}
34+
+class C {}
35+
36+
helper() {
37+
- c = C();
38+
- retained = () => c!.deleted = 'hello';
39+
return retained();
40+
}
41+
42+
*/
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"exclude": [
3+
"chrome",
4+
"d8"
5+
]
6+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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:expect/expect.dart';
6+
import 'package:reload_test/reload_test_utils.dart';
7+
8+
var retained;
9+
C? c;
10+
11+
class C {
12+
set deleted(String s) {}
13+
}
14+
15+
helper() {
16+
c = C();
17+
retained = () => c!.deleted = 'hello';
18+
return retained();
19+
}
20+
21+
Future<void> main() async {
22+
helper();
23+
await hotReload();
24+
Expect.throws(
25+
helper,
26+
(error) => '$error'.contains('Lookup failed: deleted in @setters in C'),
27+
);
28+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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:expect/expect.dart';
6+
import 'package:reload_test/reload_test_utils.dart';
7+
8+
var retained;
9+
C? c;
10+
11+
class C {}
12+
13+
helper() {
14+
return retained();
15+
}
16+
17+
Future<void> main() async {
18+
helper();
19+
await hotReload();
20+
Expect.throws(
21+
helper,
22+
(error) => '$error'.contains('Lookup failed: deleted in @setters in C'),
23+
);
24+
}
25+
26+
/** DIFF **/
27+
/*
28+
var retained;
29+
C? c;
30+
31+
-class C {
32+
- set deleted(String s) {}
33+
-}
34+
+class C {}
35+
36+
helper() {
37+
- c = C();
38+
- retained = () => c!.deleted = 'hello';
39+
return retained();
40+
}
41+
42+
*/

tests/hot_reload/call_instance_getter_return_change_ddc/main.1.dart

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ helper() {
2121
Future<void> main() async {
2222
helper();
2323
await hotReload();
24-
Expect.throws<TypeError>(helper);
24+
Expect.throws<TypeError>(
25+
helper,
26+
(error) => '$error'.contains("'double' is not a subtype of type 'String'"),
27+
);
2528
}
2629

2730
/** DIFF **/
@@ -42,13 +45,4 @@ Future<void> main() async {
4245
return retained!();
4346
}
4447
45-
Future<void> main() async {
46-
helper();
47-
await hotReload();
48-
- Expect.throws<TypeError>(
49-
- helper,
50-
- (error) => '$error'.contains("'double' is not a subtype of type 'String'"),
51-
- );
52-
+ Expect.throws<TypeError>(helper);
53-
}
5448
*/
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"exclude": [
3+
"vm"
4+
]
5+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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:expect/expect.dart';
6+
import 'package:reload_test/reload_test_utils.dart';
7+
8+
var retained;
9+
C? c;
10+
11+
class C {
12+
int _i = 0;
13+
set typeChange(int i) {
14+
_i = i;
15+
}
16+
}
17+
18+
helper() {
19+
c = C();
20+
retained = () => c!.typeChange = 99;
21+
return retained!();
22+
}
23+
24+
Future<void> main() async {
25+
helper();
26+
await hotReload();
27+
Expect.throws<TypeError>(
28+
helper,
29+
(error) => '$error'.contains("'int' is not a subtype of type 'String'"),
30+
);
31+
}

0 commit comments

Comments
 (0)