Skip to content

DotNetTypeAdapter - treat init-only properties as non-settable #16520

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

IISResetMe
Copy link
Collaborator

@IISResetMe IISResetMe commented Nov 25, 2021

PR Summary

This PR modifies the .NET type adapter to respect the IsExternalInit modifier on property setters.

IsExternalInit is the mechanism through which the C# 9.0 specification implements support for init-only setters - properties that can only be set during instantiation, after which they're considered read-only by the compiler.

By inspecting the property setter's return parameter we can similarly discover the IsExternalInit modifier on runtime types, and change the behavior of ETS to reject attempts to set init-only properties by having Adapter.PropertyIsSettable() reflect whether the modifier was found.

PR Context

As reported in #13819, PowerShell has no current facility for detecting init-only
properties, and what the C# compiler would now treat as read-only properties are suddenly writable/settable at runtime:

PS ~> $PSVersionTable['PSVersion'] -as [string]
7.2.0
PS ~> Add-Type @'
>> public class InitOnlyValue {public long Value {get; init;}}
>> '@
PS ~> $value = [InitOnlyValue]@{ Value = 123 }
PS ~> $value

Value
-----
  123

PS ~> $value.Value = 456
PS ~> $value

Value
-----
  456

PS ~> $value |Get-Member -MemberType Properties

   TypeName: InitOnlyValue

Name  MemberType Definition
----  ---------- ----------
Value Property   long Value {get;set;}

After we apply the changes in this PR, PowerShell's behavior reflects that of C# (and VB.NET):

PS ~> $value = [InitOnlyValue]@{ Value = 123 }
PS ~> $value

Value
-----
  123

PS ~> $value.Value = 456
InvalidOperation: 'Value' is a ReadOnly property.
PS ~> $value |Get-Member -MemberType Property

   TypeName: InitOnlyValue

Name  MemberType Definition
----  ---------- ----------
Value Property   long Value {get;}

PR Checklist

@IISResetMe IISResetMe changed the title WIP: DotNetTypeAdapter - treat init-only properties as non-settable DotNetTypeAdapter - treat init-only properties as non-settable Nov 25, 2021
@IISResetMe IISResetMe marked this pull request as ready for review November 25, 2021 22:48
@iSazonov iSazonov requested a review from daxian-dbw November 26, 2021 03:17
@iSazonov iSazonov added the CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log label Nov 26, 2021
@iSazonov iSazonov requested a review from vexx32 November 26, 2021 03:18
@iSazonov
Copy link
Collaborator

@SeeminglyScience @JustinGrote Please review.

Copy link
Collaborator

@SeeminglyScience SeeminglyScience left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! I echo @iSazonov's comment about maybe omitting the initOnly field. Did the hashtable conversion path (aka object init) code path not need any changes?

IISResetMe and others added 2 commits November 26, 2021 15:29
Co-authored-by: Patrick Meinecke <SeeminglyScience@users.noreply.github.com>
Co-authored-by: Patrick Meinecke <SeeminglyScience@users.noreply.github.com>
@IISResetMe
Copy link
Collaborator Author

Looks great! I echo @iSazonov's comment about maybe omitting the initOnly field. Did the hashtable conversion path (aka object init) code path not need any changes?

I'm reluctant since it makes the adapter code more readable to have the distinction IMO, but I'm happy to remove it if there are any arguments better than "technically it's not strictly needed" available.

WRT hashtable-initialization: Nope - this is the beauty of using the type adapter - from a hashtable cast, the compiler will generate an expression tree that 1) invokes the default constructor on the target type and then 2) sets the properties and fields subsequently. Since this happens directly against the runtime and not via ETS, our new settability-restriction doesn't apply

Omit test for specific (localized) error message text, not reliable
@ghost ghost added the Review - Needed The PR is being reviewed label Dec 8, 2021
@ghost
Copy link

ghost commented Dec 8, 2021

This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days.
Maintainer, please provide feedback and/or mark it as Waiting on Author

@IISResetMe
Copy link
Collaborator Author

What are we waiting for here? Anything I can do to move it forward?

@PaulHigin PaulHigin added WG-Language parser, language semantics WG-Engine core PowerShell engine, interpreter, and runtime and removed WG-Language parser, language semantics labels Jan 3, 2022
@ghost ghost removed the Review - Needed The PR is being reviewed label Jan 3, 2022
@ghost ghost added the Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept label Jan 3, 2022
@PaulHigin
Copy link
Contributor

@daxian-dbw Can you please review?

@ghost ghost removed the Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept label Jan 6, 2022
@IISResetMe
Copy link
Collaborator Author

IISResetMe commented Jan 7, 2022

Looks like the last commit pushed the "total complexity" of CoreAdapter.cs over some arbitrary threshold determined by CodeFactor.

Just to set expectations: I am NOT going to refactor 6000 lines of half-a-decade-old code as part of this PR ^_^

@ghost ghost added the Review - Needed The PR is being reviewed label Jan 15, 2022
@ghost
Copy link

ghost commented Jan 15, 2022

This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days.
Maintainer, please provide feedback and/or mark it as Waiting on Author

@IISResetMe
Copy link
Collaborator Author

@daxian-dbw *nudge nudge* :)

@daxian-dbw
Copy link
Member

@IISResetMe Sorry about the delays. I will try to get to this PR next week.

@ghost ghost removed the Review - Needed The PR is being reviewed label Jan 20, 2022
@daxian-dbw
Copy link
Member

daxian-dbw commented Jan 26, 2022

@IISResetMe Sorry that I won't be able to get to this PR this week. All my pre-allocated PR review time will most likely be spent on #12412 this week (I started the review last week and it took more time than I thought). Hopefully, I will get to this PR the next week.

@iSazonov
Copy link
Collaborator

@IISResetMe Sorry that I won't be able to get to this PR this week. All my pre-allocated PR review time will most likely be spent on #12412 this week (I started the review last week and it took more time than I thought). Hopefully, I will get to this PR the next week.

Ah! Who is the person who could double that time besides God? 😄

@IISResetMe
Copy link
Collaborator Author

Time to commence the @daxian-dbw cloning program :)

Thanks for the heads up, I fully understand

@ghost ghost added the Review - Needed The PR is being reviewed label Feb 3, 2022
@ghost
Copy link

ghost commented Feb 3, 2022

This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days.
Maintainer, please provide feedback and/or mark it as Waiting on Author

@@ -2788,6 +2788,14 @@ internal PropertyCacheEntry(PropertyInfo property)
if (propertySetter != null && (propertySetter.IsPublic || propertySetter.IsFamily))
{
this.isStatic = propertySetter.IsStatic;
foreach (Type requiredCustomModifier in propertySetter.ReturnParameter.GetRequiredCustomModifiers())
{
if (!requiredCustomModifier.IsNested && requiredCustomModifier.FullName == "System.Runtime.CompilerServices.IsExternalInit")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add comments here to indicate that this is for handling the init concept introduced in C# 9.

@@ -2788,6 +2788,14 @@ internal PropertyCacheEntry(PropertyInfo property)
if (propertySetter != null && (propertySetter.IsPublic || propertySetter.IsFamily))
{
this.isStatic = propertySetter.IsStatic;
foreach (Type requiredCustomModifier in propertySetter.ReturnParameter.GetRequiredCustomModifiers())
Copy link
Member

@daxian-dbw daxian-dbw Feb 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IISResetMe Sorry for taking so long. The changes in this PR do not prevent PSProperty from being changed through PSProperty.Value = xxx for an init property. Example (with changes in this PR):

> Add-Type -TypeDefinition @'
    public class InitOnlyTestClass
    {
        // default constructor to allow cast-initialization
        public InitOnlyTestClass()
        {
        }
    
        public long Value { get; init; }
    }
'@

> $s = [InitOnlyTestClass]::new()
> $p = $s.psobject.Properties['Value']
> $p.IsSettable
False
> $p.Value = 1000
> $p

MemberType      : Property
Value           : 1000
IsSettable      : False
IsGettable      : True
TypeNameOfValue : System.Int64
Name            : Value
IsInstance      : True

The hashtable-to-object conversion actually goes through ConvertViaNoArgumentConstructor.Convert, and depends on setting PSProperty.Value.
This is the tricky part, for an init property, PSProperty.Value should throw when setting value to it, but the [type]@{ property1 = value1; property2 = value2 } depends on setting PSProperty.Value to work. So, here we got a contradiction :(

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm almost inclined to say that's a restriction we could document. Going through PSObject to target the underlying metadata and modify the PSProperty directly is almost akin to using Reflection to modify the value in C# -- and in Reflection, too, I don't think init-only holds any water.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally don't think we should introduce a language feature that is inconsistent. If a user is doing reflection directly, then it's out of our hands. But if the user is doing it in the PowerShell way, then consistent behavior should be expected.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daxian-dbw I agree behavior should be consistent - but I'm inclined to argue that inconsistent is consistent with the behavior we're trying to mimic here.

Init-only properties are an illusion, in that it requires the consenting participation of all compilers. The C#, VB and F# implementations all introduce compile-time guardrails, no actual runtime-constraints or CTS-specific flags to indicate any behaviorial difference between properties defined with init; and auto properties. Because there is no such flags. The core design of CTS anticipates no such distinction :)

We can mimic the runtime-inconsistent behavior that the dominant .NET languages have converged on (which is what this PR attempts), or we can decide to adapt differently (because PowerShell is different and its runtime has a different lifecycle model).

Perhaps I should close this PR and leave that question up to the language WG? 😄

@ghost ghost added Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept and removed Review - Needed The PR is being reviewed labels Feb 5, 2022
Co-authored-by: Dongbo Wang <dongbow@microsoft.com>
@ghost ghost removed the Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept label Feb 5, 2022
@pull-request-quantifier-deprecated

This PR has 29 quantified lines of changes. In general, a change size of upto 200 lines is ideal for the best PR experience!


Quantification details

Label      : Extra Small
Size       : +27 -2
Percentile : 11.6%

Total files changed: 2

Change summary by file extension:
.cs : +8 -2
.ps1 : +19 -0

Change counts above are quantified counts, based on the PullRequestQuantifier customizations.

Why proper sizing of changes matters

Optimal pull request sizes drive a better predictable PR flow as they strike a
balance between between PR complexity and PR review overhead. PRs within the
optimal size (typical small, or medium sized PRs) mean:

  • Fast and predictable releases to production:
    • Optimal size changes are more likely to be reviewed faster with fewer
      iterations.
    • Similarity in low PR complexity drives similar review times.
  • Review quality is likely higher as complexity is lower:
    • Bugs are more likely to be detected.
    • Code inconsistencies are more likely to be detetcted.
  • Knowledge sharing is improved within the participants:
    • Small portions can be assimilated better.
  • Better engineering practices are exercised:
    • Solving big problems by dividing them in well contained, smaller problems.
    • Exercising separation of concerns within the code changes.

What can I do to optimize my changes

  • Use the PullRequestQuantifier to quantify your PR accurately
    • Create a context profile for your repo using the context generator
    • Exclude files that are not necessary to be reviewed or do not increase the review complexity. Example: Autogenerated code, docs, project IDE setting files, binaries, etc. Check out the Excluded section from your prquantifier.yaml context profile.
    • Understand your typical change complexity, drive towards the desired complexity by adjusting the label mapping in your prquantifier.yaml context profile.
    • Only use the labels that matter to you, see context specification to customize your prquantifier.yaml context profile.
  • Change your engineering behaviors
    • For PRs that fall outside of the desired spectrum, review the details and check if:
      • Your PR could be split in smaller, self-contained PRs instead
      • Your PR only solves one particular issue. (For example, don't refactor and code new features in the same PR).

How to interpret the change counts in git diff output

  • One line was added: +1 -0
  • One line was deleted: +0 -1
  • One line was modified: +1 -1 (git diff doesn't know about modified, it will
    interpret that line like one addition plus one deletion)
  • Change percentiles: Change characteristics (addition, deletion, modification)
    of this PR in relation to all other PRs within the repository.


Was this comment helpful? 👍  :ok_hand:  :thumbsdown: (Email)
Customize PullRequestQuantifier for this repository.

@daxian-dbw daxian-dbw added the Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept label Feb 8, 2022
@ghost ghost removed the Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept label Feb 8, 2022
@IISResetMe
Copy link
Collaborator Author

Closed based on @daxian-dbw's comments about consistency (and lack of consensus around what that means in the context of PowerShell for this specific feature)

@IISResetMe IISResetMe closed this Feb 15, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log Extra Small WG-Engine core PowerShell engine, interpreter, and runtime
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants