Extended property patterns: address PROTOTYPE comments#53594
Extended property patterns: address PROTOTYPE comments#53594jcouv merged 14 commits intodotnet:features/extended-property-patternsfrom
Conversation
|
|
||
| foreach (var subPattern in positionalPart.Subpatterns) | ||
| { | ||
| // PROTOTYPE(extended-property-patterns) ExpressionColon |
There was a problem hiding this comment.
📝 We don't need to handle ExpressionColon in positional patterns
| public void M() | ||
| { | ||
| _ = this is { Field1.Field2.Field3: {} }; | ||
| _ = this is { Field4: {} }; |
There was a problem hiding this comment.
📝 We previously didn't count this as a read on Field4 (existing bug got fixed). Previously produced: warning CS0169: The field 'C.Field4' is never used
| } | ||
|
|
||
| [Fact] | ||
| public void ExtendedPropertyPatterns_IOperation_FieldsInStructs() |
There was a problem hiding this comment.
📝 We need to discuss how to properly represent such scenarios (involving nullable value types) in IOperation and CFG. There's existing PROTOTYPE markers tracking this.
We're representing is { Field1.Field2.Field3: null } as is { Field1: { Field2: { Field 3: null } } } #Closed
| { | ||
| public unsafe void M(S* s) | ||
| { | ||
| if (0 is s->X) {} |
There was a problem hiding this comment.
📝 Previously resulted in a parsing error
| Pattern: | ||
| IRecursivePatternOperation (OperationKind.RecursivePattern, Type: null) (Syntax: '{ Field1.Fi ... ld4: null }') (InputType: A, NarrowedType: A, DeclaredSymbol: null, MatchedType: A, DeconstructSymbol: null) | ||
| DeconstructionSubpatterns (0) | ||
| PropertySubpatterns (2): |
There was a problem hiding this comment.
📝 One way we could solve the problem is representing like the following, using a new IOperation node that has an array of property references onto implicit receivers (as opposed to a chain).
IPropertySubpatternOperation (OperationKind.PropertySubpattern, Type: null) (Syntax: 'Field1.Fiel ... ield3: null')
Member:
IExtendedPropertyPatternOperation // new
Properties:
IFieldReferenceOperation: B? A.Field1 (OperationKind.FieldReference, Type: A) (Syntax: 'Field1')
Instance Receiver:
IInstanceReferenceOperation (ReferenceKind: PatternInput) (OperationKind.InstanceReference, Type: B, IsImplicit) (Syntax: 'Field1')
IFieldReferenceOperation: C? B.Field2 (OperationKind.FieldReference, Type: C) (Syntax: 'Field2')
Instance Receiver:
IInstanceReferenceOperation (ReferenceKind: PatternInput) (OperationKind.InstanceReference, Type: B, IsImplicit) (Syntax: 'Field2')
IFieldReferenceOperation: System.Object C.Field3 (OperationKind.FieldReference, Type: System.Object) (Syntax: 'Field3')
Instance Receiver:
IInstanceReferenceOperation (ReferenceKind: PatternInput) (OperationKind.InstanceReference, Type: C, IsImplicit) (Syntax: 'Field3')
#Closed
There was a problem hiding this comment.
If we decide to use ConvertedType here:
roslyn/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs
Lines 2038 to 2039 in 313caf1
I think it would make sense to also use IConversionOperation to represent a nullable value unwrap in the chain.
There was a problem hiding this comment.
From chat with Fred and Aleksey, I'll adjust the IOperation tree to take advantage of the fact that the feature is syntactic sugar. c is { Prop1.Prop2: null } -> c is { Prop1: { Prop2: null } }. We can do the same for IOperation.
| { | ||
| expr = allowExtendedProperties | ||
| ? CheckFeatureAvailability(expr, MessageID.IDS_FeatureExtendedPropertyPatterns) | ||
| : AddError(expr, ErrorCode.ERR_IdentifierExpected); |
There was a problem hiding this comment.
Instead of this I think it would be simpler to just check ExpressionColon.Expression is IdentifierName at use-sites and report. That would make this a semantic error but we're already checking invalid names there. #Resolved
| --> | ||
| <!-- PROTOTYPE(extended-property-patterns) PublicAPIs --> | ||
| <NoWarn>$(NoWarn);1573;1591;1701;RS0016</NoWarn> | ||
| <NoWarn>$(NoWarn);1573;1591;1701</NoWarn> |
There was a problem hiding this comment.
Thanks a lot for sorting this out. Have we fixed the code action or this is done manually? (I have no idea how this could be done manually) #Resolved
There was a problem hiding this comment.
I've done it semi-manually: copy sections of generated code into real files, then trigger publicAPI fixer on those, repeat... It's better than manually editting, by far :-)
| { | ||
| var subpatternSyntax = node.Subpatterns[i]; | ||
| // If the expression before the colon isn't just a name, we'll have reported a parsing error. | ||
| Debug.Assert(subpatternSyntax.NameColon is not null || subpatternSyntax.ExpressionColon is null || subpatternSyntax.ExpressionColon.HasErrors); |
There was a problem hiding this comment.
When subpatternSyntax.ExpressionColon is null NameColon is also null. #Resolved
|
@alrz @dotnet/roslyn-compiler for review. Thanks |
| } | ||
| else if (subPattern.ExpressionColon != null) | ||
| { | ||
| diagnostics.Add(ErrorCode.ERR_IdentifierExpected, subPattern.ExpressionColon.Expression.Location); |
There was a problem hiding this comment.
Clarified offline. The scenario is is (A.B: <pattern>, ...)
| } | ||
| else if (subpatternSyntax.ExpressionColon != null) | ||
| { | ||
| diagnostics.Add(ErrorCode.ERR_IdentifierExpected, subpatternSyntax.ExpressionColon.Expression.Location); |
| { | ||
| while (member is not null && !member.HasErrors) | ||
| { | ||
| if (_sourceAssembly is not null && member.Symbol is FieldSymbol field) |
| { | ||
| if (sub is BoundPropertySubpattern { Member: var member }) | ||
| { | ||
| while (member is not null && !member.HasErrors) |
| // `.LastProp: <pattern operation>` portion (treated as `{ LastProp: <pattern operation> }`) | ||
| IPatternOperation pattern = (IPatternOperation)Create(subpattern.Pattern); | ||
| result = createPropertySubpattern(member.Symbol, pattern, inputType, nameSyntax, isSingle: member.Receiver is null); | ||
| } |
There was a problem hiding this comment.
Consider moving this case before the do { ... } while (...); loop. It doesn't look like it shares anything with the rest of the loop. #Closed
| } | ||
| else | ||
| { | ||
| // Create an operation for a preceding property acess: |
|
|
||
| int getExtendedPropertySlot(BoundPropertySubpatternMember member, int inputSlot) | ||
| { | ||
| if (member is null || member.Symbol is null) |
| // or | ||
| // `.LastProp: <pattern operation>` portion (treated as `{ LastProp: <pattern operation> }`) | ||
| var nameSyntax = member.Syntax; | ||
| var inputType = member.Receiver?.Type.StrippedType().GetPublicSymbol() ?? matchedType; |
There was a problem hiding this comment.
| Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: True, IsUserDefined: False) (MethodSymbol: null) | ||
| Operand: | ||
| ILiteralOperation (OperationKind.Literal, Type: null, Constant: null) (Syntax: 'null') | ||
| IPropertySubpatternOperation (OperationKind.PropertySubpattern, Type: null, IsImplicit) (Syntax: 'prop') |
There was a problem hiding this comment.
Only one IOperation node can be explicit for a given piece of syntax. The property reference below is the IOperation node associated to that syntax which isn't implicit.
| var compilation = CreateCompilation(source, parseOptions: TestOptions.Regular9); | ||
| compilation.VerifyEmitDiagnostics( | ||
| // (8,22): error CS8652: The feature 'extended property patterns' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. | ||
| // _ = this is (Property.Property: null, Property: null); |
There was a problem hiding this comment.
Both positional and recurisve patterns use the same syntax node SubpatternSyntax and share the same parsing logic (ParseSubpatternElement).
There's no reason beyond that.
| var compilation = CreateCompilation(program, parseOptions: TestOptions.RegularWithExtendedPropertyPatterns); | ||
| compilation.VerifyEmitDiagnostics( | ||
| // (7,21): error CS0122: 'C1.Prop1' is inaccessible due to its protection level | ||
| // _ = c1 is { Prop1.Prop2: null }; |
There was a problem hiding this comment.
Should we report an error on Prop2 as well?
It looks like we don't report an error on Prop2 on a similar dotted reference outside of patterns (see sharplab.io) so this is consistent, but the missing second error in both cases seem unexpected. #WontFix
There was a problem hiding this comment.
That is not part of this PR, and because the behavior matches pre-existing member access behavior, I'll leave as-is.
I don't think this is simple to fix. BindInstanceMemberAccess returns with a null ExpressionSymbol in this scenario, so when we get to Prop2 we're only looking at an error type...
| public class FlowAnalysisTests : FlowTestBase | ||
| { | ||
| [Fact] | ||
| public void RegionInIsPattern01() |
There was a problem hiding this comment.
An IDE test crashed in an earlier iteration of this PR. I created a compiler-level repro. It follows many other flow analysis tests, but I prefered to keep it together with the rest of the feature.
| char[] newLineChars = Environment.NewLine.ToCharArray(); | ||
| string actual = actualOperationTree.Trim(newLineChars); | ||
| actual = actual.Replace("\"", "\"\""); | ||
| actual = actual.Replace("\"", "\"\"").Replace(" \n", "\n").Replace(" \r", "\r"); |
There was a problem hiding this comment.
I've been annoyed for a long time about the extra space output by the IOperation tests. This PR has a number of IOperation tests where I ran into this issue.
This change allows the space to be ignored in comparison and removed in "actual".
| } | ||
|
|
||
| return result.ToImmutableAndFree(); | ||
| return result.ToImmutableAndFree(); |
There was a problem hiding this comment.
Consider extracting a helper method for use here and in if above. #Closed
|
@333fred @dotnet/roslyn-compiler for a second review. Thanks |
|
|
||
| IEnumerable<TypeInferenceInfo> InferTypeInSubpatternCore(ExpressionSyntax expression) | ||
| { | ||
| var result = ArrayBuilder<TypeInferenceInfo>.GetInstance(); |
There was a problem hiding this comment.
| var result = ArrayBuilder<TypeInferenceInfo>.GetInstance(); | |
| using var result = TemporaryAray<TypeInferenceInfo>.Empty; | |
| ``` #Resolved |
There was a problem hiding this comment.
TemporaryArray is preferred when the expected case is a small number of results (4 or less).
| @@ -1478,8 +1492,6 @@ private IEnumerable<TypeInferenceInfo> InferTypeInSubpattern( | |||
|
|
|||
| return result.ToImmutableAndFree(); | |||
There was a problem hiding this comment.
| return result.ToImmutableAndFree(); | |
| return result.ToImmutableAndClear(); | |
| ``` #Resolved |
| { | ||
| case var x when e.Property1.Property2.ToString() == null: // 1 | ||
| break; | ||
| case { Property1.Property2: not null }: |
333fred
left a comment
There was a problem hiding this comment.
Compiler side LGTM (commit 12)
| if (subpattern.NameColon != null) | ||
| { | ||
| return InferTypeInSubpatternCore(subpattern.NameColon.Name); | ||
| } | ||
| else if (subpattern.ExpressionColon is { Expression: MemberAccessExpressionSyntax memberAccess }) | ||
| { | ||
| return InferTypeInSubpatternCore(memberAccess.Name); | ||
| } |
There was a problem hiding this comment.
nit: You could probably just call InferTypeInSubpatternCore(subpattern.ExpressionColon.Expression) instead of these two ifs. We've verified GetSymbolInfo works with a member access. #Resolved
| if (this is c!) {} | ||
| if (this is (c!)) {} | ||
| if (this is Type!) {} // 1 | ||
| if (this is ContainerType!.Type) {} // 2 | ||
| if (this is ContainerType.Type!) {} // 3 |
There was a problem hiding this comment.
@jcouv Why is it expected to accept suppression in the first two?
There was a problem hiding this comment.
Those already compile (in C# 9).
There was a problem hiding this comment.
I get parsing errors on sharplab default branch.
I think patterns did not accept suppression in the previous version of the compiler.
There was a problem hiding this comment.
Oops, I used sharplab on main, which already has this change...
Filed #54071
Addresses the remaining PROTOTYPE markers and work items.
Fixes #52956 (tracking usage of fields)
Fixes #53484 (deal with fallout of better error recovery in patterns parsing)
IDE changes: adds ability to infer type and generate variable
c is { Property.|[YetUndefined|]: ... }Test plan #52468