Skip to content

Commit 0bd261c

Browse files
committed
fix(runtime): three attribute shapes match .NET (#1980, group G-4)
Rule-14 sweep. The point of this group is that the port was wrong in BOTH directions, which is why one rule could not fix it: * CompilerFeatureRequiredAttribute published a full IsOptional setter where .NET's is { get; init; } -- too permissive; * ObsoletedOSPlatformAttribute and RequiresPreviewFeaturesAttribute took a `url` constructor parameter .NET does not declare and fed it into a read-only accessor where .NET's is { get; set; } -- too inventive on the constructor and too restrictive on the property. `init` is two facts, not one: the value CAN be supplied at construction and CANNOT be assigned afterwards. C++ has no init, and the analogue of that pair is a constructor parameter with no setter -- so removing the setter alone would have been a narrowing rather than a translation. The two-argument constructor is what makes the removal faithful, and one test asserts both halves together. Landed under SA-8 (mutability) and SA-5 (Url), with SA-2's five conditions. Five mutations, all caught, two at compile time. M4 and M5 are run once per type deliberately -- two separate declarations, and fixing one while leaving the other is the easy half-repair. M4 was invalid as first written and was reformulated rather than counted. The absence pin uses a dependent parameter (the #2299 gcc trap). Fixture set 41/215 -> 42/219; site 4 is the trait query, which breaks a consumer silently. First-party migration was two sites, both tests, both found by the compiler. Downstream measured: 0 sites in cna, 0 in mobile-eggbert. #1980 stays open for G-3 (vtable) and G-5. Gate: 17,506 run, 17,506 passed, 0 failed, 0 skipped across 38 executables (+3 on 17,503; SharpRuntimeTests_Runtime 186 -> 189; no other executable moved). Module graph unchanged at 41/93.
1 parent 17063c8 commit 0bd261c

7 files changed

Lines changed: 323 additions & 16 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<!-- SPDX-License-Identifier: MIT -->
2+
<!-- Copyright (c) Robert Vokac and contributors -->
3+
4+
# Migration — three attribute shapes match .NET (ticket #1980, group G-4)
5+
6+
*2026-08-19.* `CompilerFeatureRequiredAttribute` loses its `IsOptional` setter and gains a
7+
two-argument constructor; `ObsoletedOSPlatformAttribute` and `RequiresPreviewFeaturesAttribute`
8+
lose their `url` constructor parameter and gain a `Url` setter.
9+
10+
**This is a public source break**, landed under **SA-8** (the mutability half) and **SA-5** (the
11+
`Url` half), with SA-2's five conditions discharged. No layout or vtable change.
12+
13+
---
14+
15+
## 1. The port was wrong in **both** directions
16+
17+
That is the point of this group, and it is why the two halves could not be fixed by one rule.
18+
19+
| | Port had | .NET has | Port was |
20+
|---|---|---|---|
21+
| `CompilerFeatureRequiredAttribute::IsOptional` | full setter | `{ get; init; }` | **too permissive** |
22+
| `ObsoletedOSPlatformAttribute::Url` | `url` ctor param, read-only accessor | no such param; `{ get; set; }` | **too restrictive** on the property, **inventive** on the constructor |
23+
| `RequiresPreviewFeaturesAttribute::Url` | same | same | same |
24+
25+
## 2. `init` is two facts, not one
26+
27+
.NET's `public bool IsOptional { get; init; }` means the value **can** be supplied at
28+
construction —
29+
30+
```csharp
31+
new CompilerFeatureRequiredAttribute("RefStructs") { IsOptional = true } // legal
32+
```
33+
34+
— and **cannot** be assigned afterwards. C++ has no `init`, and the exact analogue of that pair is
35+
a **constructor parameter with no setter**.
36+
37+
So removing `setIsOptionalProperty` *alone* would have been a narrowing, not a translation: the
38+
value would have become unsettable. The two-argument constructor is what makes the removal
39+
faithful. A test asserts both halves together, and the absence pin uses a **dependent** parameter,
40+
because gcc evaluates a non-dependent `requires` eagerly and hard-errors instead of yielding
41+
`false` — the #2299 trap.
42+
43+
## 3. `Url` was inverted
44+
45+
.NET declares exactly:
46+
47+
```csharp
48+
public ObsoletedOSPlatformAttribute(string platformName) // PlatformAttributes.cs
49+
public ObsoletedOSPlatformAttribute(string platformName, string? message)
50+
public string? Message { get; }
51+
public string? Url { get; set; }
52+
53+
public RequiresPreviewFeaturesAttribute() // …Attribute.cs:28
54+
public RequiresPreviewFeaturesAttribute(string? message) // …Attribute.cs:34
55+
public string? Url { get; set; } // …Attribute.cs:47
56+
```
57+
58+
There is **no** `url` constructor parameter in either type, and `Url` is **settable**. This port
59+
had a third (respectively second) parameter .NET does not declare, feeding an accessor that .NET
60+
makes writable.
61+
62+
## 4. To migrate
63+
64+
```cpp
65+
// before
66+
CompilerFeatureRequiredAttribute f("RefStructs");
67+
f.setIsOptionalProperty(true);
68+
ObsoletedOSPlatformAttribute o("ios", "Use X", "https://example.com");
69+
RequiresPreviewFeaturesAttribute p("Preview", "https://aka.ms/preview");
70+
71+
// after
72+
CompilerFeatureRequiredAttribute f("RefStructs", true);
73+
ObsoletedOSPlatformAttribute o("ios", "Use X");
74+
o.setUrlProperty("https://example.com");
75+
RequiresPreviewFeaturesAttribute p("Preview");
76+
p.setUrlProperty("https://aka.ms/preview");
77+
```
78+
79+
**First-party migration was two sites**, both tests, and the compiler found both. Nothing in any
80+
`modules/*/src` uses these types.
81+
82+
## 5. Evidence
83+
84+
Five mutations, **all caught**, two at compile time:
85+
86+
| Mutation | Caught by |
87+
|---|---|
88+
| M1 — `setIsOptionalProperty` reinstated | `Decl1980G4_IsOptionalIsNotMutableAfterConstruction` (compile time) |
89+
| M2 — the two-argument constructor ignores its second argument | `StoresFeatureNameAndOptionalFlag` |
90+
| M3 — the `url` constructor parameter restored | `Fix1980G4_UrlIsASettablePropertyNotAConstructorArgument` (compile time) |
91+
| M4 — `ObsoletedOSPlatformAttribute::setUrlProperty` silently does nothing | that type's own case |
92+
| M5 — `RequiresPreviewFeaturesAttribute::setUrlProperty` silently does nothing | that type's own case |
93+
94+
M4 and M5 are run **once per type** on purpose: they are two separate declarations, and fixing
95+
one while leaving the other is the easy half-repair. M4 was invalid as first written — the anchor
96+
spanned two doc-comments that differ between the types — and was reformulated rather than counted.
97+
98+
Negative consumer fixture: `test/consumer/runtime_g4_attribute_shape_negative.cpp`, four sites,
99+
all rejected. Fixture set grows to **42 fixtures / 219 sites**. Site 4 is the trait query — the
100+
shape that breaks a consumer **silently** rather than at the call site.
101+
102+
Gate: **17,506 run, 17,506 passed, 0 failed, 0 skipped** across 38 executables — `+3` on 17,503
103+
(`SharpRuntimeTests_Runtime` 186 → 189; two pins migrated in place, three cases added). No other
104+
executable moved. Module graph unchanged at 41/93.
105+
106+
## 6. Downstream, measured
107+
108+
All three types appear in **zero** places in `cna` and **zero** in `mobile-eggbert`. Neither
109+
repository was modified, and no downstream ticket is needed.
110+
111+
## 7. Scope
112+
113+
This closes **G-4** of #1980. G-1 and G-2 landed earlier the same day. Remaining: **G-3**
114+
(reparenting and sealing — a vtable *and* layout change SA-3 excludes) and **G-5** / SR-AUD-167
115+
(retyping `MarshalAs` fields, adding `ComInterfaceType`/`ClassInterfaceType`).

modules/runtime/include/System/Runtime/CompilerServices/CompilerFeatureRequiredAttribute.hpp

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,29 @@ class CompilerFeatureRequiredAttribute final : public System::Attribute {
3131
explicit CompilerFeatureRequiredAttribute(std::string featureName)
3232
: featureName_(std::move(featureName)) {}
3333

34+
/**
35+
* Initializes the attribute with its feature name and optionality.
36+
*
37+
* #1980 group G-4 / SR-AUD-160. .NET declares `public bool IsOptional { get; init; }`
38+
* (`CompilerFeatureRequiredAttribute.cs`), and `init` means the value can be supplied **at
39+
* construction and never afterwards** -- `new CompilerFeatureRequiredAttribute("X") {
40+
* IsOptional = true }` is legal, assigning to it later is not. C++ has no `init`, and the
41+
* exact analogue of that pair of facts is a constructor parameter with no setter. This
42+
* overload is what makes removing `setIsOptionalProperty` a *translation* rather than a
43+
* narrowing: the value is still settable, just no longer mutable.
44+
*/
45+
CompilerFeatureRequiredAttribute(std::string featureName, bool isOptional)
46+
: featureName_(std::move(featureName)), isOptional_(isOptional) {}
47+
3448
/** Gets the required compiler feature's name. */
3549
[[nodiscard]] const std::string& getFeatureNameProperty() const noexcept { return featureName_; }
3650

3751
/** Gets whether a compiler may ignore an unknown feature name. */
3852
[[nodiscard]] bool getIsOptionalProperty() const noexcept { return isOptional_; }
3953

40-
/** Sets whether a compiler may ignore an unknown feature name. */
41-
void setIsOptionalProperty(bool value) noexcept { isOptional_ = value; }
54+
// #1980 G-4 / SR-AUD-160: setIsOptionalProperty is GONE. .NET's IsOptional is `{ get; init; }`
55+
// -- settable at construction, immutable afterwards -- so a full setter published a mutability
56+
// .NET does not have. Supply the value through the two-argument constructor above.
4257
};
4358

4459
} // namespace System::Runtime::CompilerServices

modules/runtime/include/System/Runtime/Versioning/VersioningAttributes.hpp

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,21 +100,34 @@ namespace System::Runtime::Versioning {
100100
/**
101101
* @param platformName Platform on which the API is obsolete.
102102
* @param message Optional deprecation message.
103-
* @param url Optional URL with migration guidance.
103+
*
104+
* #1980 group G-4 / SR-AUD-164. **There is no `url` parameter, deliberately.** .NET
105+
* declares exactly two constructors -- `(platformName)` and `(platformName, message)`
106+
* (`PlatformAttributes.cs`) -- and exposes the URL as a **settable property**,
107+
* `public string? Url { get; set; }`. This port had it the other way round: a third
108+
* constructor parameter .NET does not have, feeding a read-only accessor. Both halves
109+
* were wrong, and in opposite directions.
104110
*/
105111
explicit ObsoletedOSPlatformAttribute(const std::string& platformName,
106-
const std::string& message = {},
107-
const std::string& url = {})
108-
: platformName_(platformName), message_(message), url_(url) {}
112+
const std::string& message = {})
113+
: platformName_(platformName), message_(message) {}
109114

110115
/** @return The platform identifier. */
111116
[[nodiscard]] const std::string& getPlatformNameProperty() const { return platformName_; }
112117

113118
/** @return The deprecation message, or empty if not provided. */
114119
[[nodiscard]] const std::string& getMessageProperty() const { return message_; }
115120

116-
/** @return The migration URL, or empty if not provided. */
121+
/** @return The migration URL, or empty if not set. */
117122
[[nodiscard]] const std::string& getUrlProperty() const { return url_; }
123+
124+
/**
125+
* @brief Sets the URL that provides more information about the obsolescence.
126+
*
127+
* #1980 G-4 / SR-AUD-164: .NET's `Url` is `{ get; set; }` -- a fully settable property,
128+
* not a constructor parameter.
129+
*/
130+
void setUrlProperty(const std::string& value) { url_ = value; }
118131
};
119132

120133
/** Marks an API as requiring preview features that may change in future releases. */
@@ -127,16 +140,27 @@ namespace System::Runtime::Versioning {
127140

128141
/**
129142
* @param message Explanation of why the API is preview.
130-
* @param url Optional URL with more information.
143+
*
144+
* #1980 group G-4 / SR-AUD-164. **No `url` parameter**, matching .NET's
145+
* `public RequiresPreviewFeaturesAttribute(string? message)`
146+
* (`RequiresPreviewFeaturesAttribute.cs:34`); the URL is a settable property there.
131147
*/
132-
explicit RequiresPreviewFeaturesAttribute(const std::string& message, const std::string& url = {})
133-
: message_(message), url_(url) {}
148+
explicit RequiresPreviewFeaturesAttribute(const std::string& message)
149+
: message_(message) {}
134150

135151
/** @return The informational message, or empty if not provided. */
136152
[[nodiscard]] const std::string& getMessageProperty() const { return message_; }
137153

138-
/** @return The informational URL, or empty if not provided. */
154+
/** @return The informational URL, or empty if not set. */
139155
[[nodiscard]] const std::string& getUrlProperty() const { return url_; }
156+
157+
/**
158+
* @brief Sets the URL that provides more information.
159+
*
160+
* #1980 G-4 / SR-AUD-164: .NET's `Url` is `{ get; set; }`
161+
* (`RequiresPreviewFeaturesAttribute.cs:47`).
162+
*/
163+
void setUrlProperty(const std::string& value) { url_ = value; }
140164
};
141165

142166
} // namespace System::Runtime::Versioning

modules/runtime/tests/System/Runtime/RuntimeTests.cpp

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,39 @@ TEST(StateMachineAttributeTests, AsyncAndIteratorAttributes_InheritStateMachineT
122122
System::Type::From<char>().getNameProperty());
123123
}
124124

125+
namespace {
126+
/// Dependent on purpose: gcc evaluates a NON-dependent `requires` eagerly and hard-errors on
127+
/// the missing member instead of yielding false (the #2299 trap).
128+
template <typename T>
129+
concept HasSetIsOptional = requires(T a) { a.setIsOptionalProperty(true); };
130+
}
131+
125132
TEST(CompilerFeatureRequiredAttributeTests, StoresFeatureNameAndOptionalFlag) {
133+
// MIGRATED by #1980 group G-4 / SR-AUD-160. The old body called setIsOptionalProperty, which
134+
// published a mutability .NET does not have: its IsOptional is `{ get; init; }` -- settable at
135+
// construction, immutable afterwards. The value is now supplied through the constructor.
126136
CompilerFeatureRequiredAttribute attr("custom-feature");
127137
EXPECT_EQ(attr.getFeatureNameProperty(), "custom-feature");
128138
EXPECT_FALSE(attr.getIsOptionalProperty());
129-
attr.setIsOptionalProperty(true);
130-
EXPECT_TRUE(attr.getIsOptionalProperty());
139+
140+
CompilerFeatureRequiredAttribute optional("custom-feature", true);
141+
EXPECT_EQ(optional.getFeatureNameProperty(), "custom-feature");
142+
EXPECT_TRUE(optional.getIsOptionalProperty());
143+
131144
EXPECT_EQ(CompilerFeatureRequiredAttribute::RefStructs, "RefStructs");
132145
EXPECT_EQ(CompilerFeatureRequiredAttribute::RequiredMembers, "RequiredMembers");
133146
}
134147

148+
TEST(CompilerFeatureRequiredAttributeTests, Decl1980G4_IsOptionalIsNotMutableAfterConstruction) {
149+
// `init` is settable-at-construction AND immutable-afterwards. Removing the setter without
150+
// adding the constructor would have been a narrowing; keeping the setter would have published
151+
// mutability .NET lacks. This asserts both halves at once.
152+
static_assert(std::is_constructible_v<CompilerFeatureRequiredAttribute, std::string, bool>,
153+
"#1980 G-4: the value must still be settable at construction");
154+
static_assert(!HasSetIsOptional<CompilerFeatureRequiredAttribute>,
155+
"#1980 G-4: .NET's IsOptional is init-only, so there is no setter");
156+
}
157+
135158
TEST(CompilerMetadataMarkerAttributeTests, MarkersInstantiateAndDeriveFromAttribute) {
136159
ExtensionAttribute extension;
137160
RequiredMemberAttribute required;
@@ -640,11 +663,29 @@ TEST(UnsupportedOSPlatformGuardAttributeTests, Constructor_StoresPlatform) {
640663
EXPECT_EQ(attr.getPlatformNameProperty(), "windows");
641664
}
642665

643-
TEST(ObsoletedOSPlatformAttributeTests, Constructor_AllFields) {
644-
ObsoletedOSPlatformAttribute attr("ios", "Use X instead", "https://example.com");
666+
TEST(ObsoletedOSPlatformAttributeTests, Fix1980G4_UrlIsASettablePropertyNotAConstructorArgument) {
667+
// MIGRATED by #1980 group G-4 / SR-AUD-164, and the port had BOTH halves wrong, in OPPOSITE
668+
// directions: it took a third constructor parameter .NET does not have, and fed it into a
669+
// read-only accessor where .NET's is `public string? Url { get; set; }`. .NET declares
670+
// exactly `(platformName)` and `(platformName, message)` (PlatformAttributes.cs).
671+
ObsoletedOSPlatformAttribute attr("ios", "Use X instead");
645672
EXPECT_EQ(attr.getPlatformNameProperty(), "ios");
646673
EXPECT_EQ(attr.getMessageProperty(), "Use X instead");
674+
EXPECT_EQ(attr.getUrlProperty(), "") << "unset until assigned, as .NET's null is";
675+
676+
attr.setUrlProperty("https://example.com");
647677
EXPECT_EQ(attr.getUrlProperty(), "https://example.com");
678+
679+
static_assert(!std::is_constructible_v<ObsoletedOSPlatformAttribute,
680+
std::string, std::string, std::string>,
681+
"#1980 G-4: .NET has no three-argument constructor");
682+
}
683+
684+
TEST(ObsoletedOSPlatformAttributeTests, Fix1980G4_TheOneArgumentConstructorIsDotNets) {
685+
ObsoletedOSPlatformAttribute attr("android");
686+
EXPECT_EQ(attr.getPlatformNameProperty(), "android");
687+
EXPECT_EQ(attr.getMessageProperty(), "");
688+
EXPECT_EQ(attr.getUrlProperty(), "");
648689
}
649690

650691
TEST(RequiresPreviewFeaturesAttributeTests, DefaultConstructor_EmptyMessage) {
@@ -657,6 +698,20 @@ TEST(RequiresPreviewFeaturesAttributeTests, Constructor_WithMessage) {
657698
EXPECT_EQ(attr.getMessageProperty(), "Preview feature");
658699
}
659700

701+
TEST(RequiresPreviewFeaturesAttributeTests, Fix1980G4_UrlIsASettablePropertyNotAConstructorArgument) {
702+
// The same inversion as ObsoletedOSPlatformAttribute, in the second of the two types G-4
703+
// names. .NET: `public RequiresPreviewFeaturesAttribute(string? message)` and
704+
// `public string? Url { get; set; }` (RequiresPreviewFeaturesAttribute.cs:34,47).
705+
RequiresPreviewFeaturesAttribute attr("Preview feature");
706+
EXPECT_EQ(attr.getUrlProperty(), "");
707+
attr.setUrlProperty("https://aka.ms/preview");
708+
EXPECT_EQ(attr.getUrlProperty(), "https://aka.ms/preview");
709+
710+
static_assert(!std::is_constructible_v<RequiresPreviewFeaturesAttribute,
711+
std::string, std::string>,
712+
"#1980 G-4: .NET's constructor takes the message alone");
713+
}
714+
660715
// ===========================================================================
661716
// CompilerGeneratedAttribute
662717
// ===========================================================================

plan.sqlite3

0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)