Commit 73ccc95
Fix bug when ensuring union
Summary:
# Problem
A long-standing bug affected Thrift unions used as optional fields. When ensuring different fields in a union, the patch logic would incorrectly remove the entire union from its parent struct. For example:
```
union Bar {
1: i32 field_1;
2: i32 field_2;
}
struct Foo {
1: optional Bar bar;
}
```
If you performed:
```
Foo.patch<ident::bar>().ensure<ident::field_1>(10);
Foo.patch<ident::bar>().ensure<ident::field_2>(20);
```
The patch would incorrectly translate to:
```
Foo.remove<ident::bar>();
```
# Why?
The union's ensure semantic clears all fields before ensuring a new one. E.g.,
```
MyUnion u;
u.field_1().ensure();
u.field_2().ensure();
EXPECT_FALSE(u.field_1().has_value());
```
In this case we ensured `field_1` first. When ensuring `field_2`, the existing code saw previously we ensured `field_1`, thus it will first clear all fields, then ensure field_2. In another word,
```
Foo.patch<ident::bar>().ensure<ident::field_2>(20);
```
this will be translated to
```
auto& patch = Foo.patch<ident::bar>();
patch.clear();
patch.ensure<ident::field_2>(20);
```
The problem is that `Foo.patch<ident::bar>().clear()` has a different meaning when `bar` is an optional field: it means removing the field from the parent struct. Thus the patch would be equivalent to
```
Foo.remove<ident::bar>();
```
which is not the intended behavior.
# Fix
The patch changes the behavior so that ensuring a different field in a union now uses assign instead of clear. e.g., if `field_1` is ensured previously, the following operation
```
Foo.patch<ident::bar>().ensure<ident::field_2>(20);
```
is now translated to:
```
Foo.patch<ident::bar>().assign<ident::field_2>(20);
```
# Another problem
To avoid similar problems, we should never translate other patch operations into `clear`, since `clear` will remove the data from the parent struct, which is not what we want.
I found that we also translate `assign` operation in union to `clear`, e.g.,
```
MyUnionPatch patch;
patch.assign(foo);
patch.patchIfSet<Id>();
```
when we saw `patchIfSet`, we will call `ensurePatchable` which translate `patch.assign(foo)` into
```
patch.clear();
patch.ensure(foo);
```
This is also problematic (see unit-test for concrete example). This diff changed so that it will be translated to the following code instead
```
patch.patch<Id>().assign(active field in foo);
```
Which is basically an ensure + assign.
Reviewed By: praihan
Differential Revision: D88342407
fbshipit-source-id: 082e4022ec192c0982c25905ead34043de9700df1 parent 62dc4f3 commit 73ccc95
1 file changed
Lines changed: 49 additions & 5 deletions
Lines changed: 49 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
345 | 345 | | |
346 | 346 | | |
347 | 347 | | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
348 | 363 | | |
349 | 364 | | |
350 | 365 | | |
| |||
355 | 370 | | |
356 | 371 | | |
357 | 372 | | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
358 | 383 | | |
359 | 384 | | |
360 | 385 | | |
| |||
591 | 616 | | |
592 | 617 | | |
593 | 618 | | |
594 | | - | |
595 | | - | |
596 | | - | |
597 | | - | |
598 | | - | |
| 619 | + | |
| 620 | + | |
599 | 621 | | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
600 | 644 | | |
601 | 645 | | |
602 | 646 | | |
| |||
0 commit comments