-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Add support for generating fields/properties for property patterns. #10873
Add support for generating fields/properties for property patterns. #10873
Conversation
Tagging @dotnet/roslyn-ide |
@CyrusNajmabadi This will have to wait until after #10866; I'm removing property patterns from the code base before insertion into future, and any changes to the pattern-matching code will cause merge heck. |
@CyrusNajmabadi, @gafter: Should we close this ancient PR until we're ready for property patterns? #Closed |
Works for me. If we add property patterns back in someone can resurrect this :) #Closed |
@gafter Can you give me a set of interesting examples that demonstrate how people can use recursive patterns? I want to make sure this features handles all the ones that make sense. Thanks! #Closed |
Yes, I will prepare a set of examples. #Closed |
Awesome, thanks. It doesn't have to be huge. Just like 1 (or 2) tops, for each interesting new construct. And maybe some special 'oddities' (if they exist) to be aware of. #Closed |
I will include some complete examples here in a moment, but just to point out some oddities first... A property pattern checks that the input value is non-null, and that each named property matches the given pattern. But you can use no patterns if you just want a non-null check:
The |
Good case. Just to verify, you should be able to write the following right:
And this just means: "'o' is non-null and has a field/property 'X' that matches the pattern 'int i'"? #Resolved |
Also, do we have a list of all patterns written down somewhere, i'd like to test out a variety of combinations. What i know of off the top of my head:
Did we do deconstruction patterns? i.e. something like #Resolved |
Here is a little sample demonstrating the interaction of tuples and patterns. Possibly not relevant to this PR using System;
public class Door
{
public DoorState State;
public enum DoorState { Opened, Closed, Locked }
public enum Action { Open, Close, Lock, Unlock }
public void Act(Action action, bool haveKey = false)
{
Console.Write($"{State} {action}{(haveKey ? " withKey" : null)}");
State = ChangeState0(State, action, haveKey);
Console.WriteLine($" -> {State}");
}
public static DoorState ChangeState0(DoorState state, Action action, bool haveKey = false)
{
switch ((state, action))
{
case (DoorState.Opened, Action.Close):
return DoorState.Closed;
case (DoorState.Closed, Action.Open):
return DoorState.Opened;
case (DoorState.Closed, Action.Lock) when haveKey:
return DoorState.Locked;
case (DoorState.Locked, Action.Unlock) when haveKey:
return DoorState.Closed;
case var (oldState, _):
return oldState;
}
}
public static DoorState ChangeState1(DoorState state, Action action, bool haveKey = false) =>
(state, action) switch {
(DoorState.Opened, Action.Close) => DoorState.Closed,
(DoorState.Closed, Action.Open) => DoorState.Opened,
(DoorState.Closed, Action.Lock) when haveKey => DoorState.Locked,
(DoorState.Locked, Action.Unlock) when haveKey => DoorState.Closed,
_ => state };
}
class Program
{
static void Main(string[] args)
{
var door = new Door();
door.Act(Door.Action.Close);
door.Act(Door.Action.Lock);
door.Act(Door.Action.Lock, true);
door.Act(Door.Action.Open);
door.Act(Door.Action.Unlock);
door.Act(Door.Action.Unlock, true);
door.Act(Door.Action.Open);
}
} #Resolved |
Also, inside a property-pattern, what syntax are the field/property pattern matchers? At one point it was |
Here is a possibly more relevant sample, though there are only a couple of examples of property patterns in it. using System;
class Program2
{
static StatementSyntax s =
new ExpressionStatementSyntax(
new AssignmentExpressionSyntax(
new SimpleNameSyntax(new SyntaxToken("x")),
new SimpleNameSyntax(new SyntaxToken("x"))));
public static void Test()
{
if (s is
ExpressionStatementSyntax(
AssignmentExpressionSyntax(
SimpleNameSyntax(SyntaxToken(var leftName)),
SimpleNameSyntax(SyntaxToken(var rightName))
) a) &&
leftName == rightName)
{
Console.WriteLine("self-assignment!");
}
}
public static void Test2()
{
if (s is
ExpressionStatementSyntax(
AssignmentExpressionSyntax(
SimpleNameSyntax { Identifier: SyntaxToken(var leftName) },
SimpleNameSyntax { Identifier: SyntaxToken(var rightName) }
) a) &&
leftName == rightName)
{
Console.WriteLine("self-assignment!");
}
}
public static void Test3()
{
if (s is
ExpressionStatementSyntax(
AssignmentExpressionSyntax(
SimpleNameSyntax(var leftToken),
SimpleNameSyntax(var rightToken)
) a) &&
leftToken.ValueText == rightToken.ValueText)
{
Console.WriteLine("self-assignment!");
}
}
}
abstract class SyntaxNode
{
public int Location => 0;
}
class SyntaxToken : SyntaxNode
{
public string ValueText;
public SyntaxToken(string ValueText) => this.ValueText = ValueText;
public void Deconstruct(out string ValueText) => ValueText = this.ValueText;
}
class StatementSyntax : SyntaxNode
{
}
class ExpressionSyntax : SyntaxNode
{
}
class ExpressionStatementSyntax : StatementSyntax
{
public ExpressionSyntax Expression;
public ExpressionStatementSyntax(ExpressionSyntax Expression) => this.Expression = Expression;
public void Deconstruct(out ExpressionSyntax Expression) => Expression = this.Expression;
}
class AssignmentExpressionSyntax : ExpressionSyntax
{
public ExpressionSyntax Left;
public ExpressionSyntax Right;
public AssignmentExpressionSyntax(ExpressionSyntax Left, ExpressionSyntax Right) => (this.Left, this.Right) = (Left, Right);
public void Deconstruct(out ExpressionSyntax Left, out ExpressionSyntax Right) => (Left, Right) = (this.Left, this.Right);
}
class SimpleNameSyntax : ExpressionSyntax
{
public SyntaxToken Identifier;
public SimpleNameSyntax() { }
public SimpleNameSyntax(SyntaxToken Identifier) => this.Identifier = Identifier;
public void Deconstruct(out SyntaxToken Identifier) => Identifier = this.Identifier;
} #Resolved |
Ah interesting: is ExpressionStatementSyntax(
AssignmentExpressionSyntax(
SimpleNameSyntax { Identifier: SyntaxToken(var leftName) },
SimpleNameSyntax { Identifier: SyntaxToken(var rightName) }
) a) So the "ExpressionStatementSyntax ( .. )``` is a type+deconstruction pattern? Ensure that the value is of htat type, and if so, deconstruct it and check if the deconstructed parts match teh subpatterns? Then |
Yes, you've got that. In its most general form, a recursive pattern can be
You can omit any of the parts, except that you can't have only a type, only an identifier, or nothing. |
See also dotnet/csharplang#1054 and note that we added a shorthand discard pattern |
…nto generateVarPatterns
Tagging @dotnet/roslyn-ide Please review this update of "generate variable" to understand and properly generate properties when used in property-patterns. |
|
||
class Blah | ||
{ | ||
public int X { get; internal set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
internal
is redundant here
public int X { get; internal set; } | ||
} | ||
}", parseOptions: WithPatternsEnabled); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider adding tests with some error cases, eg. Blah { X: { [|Y|]: int i } }
where X doesn't exist, and consider adding an example with more than 1 property
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider adding tests with some error cases, eg. Blah { X: { [|Y|]: int i } } where X doesn't exist,
sure
and consider adding an example with more than 1 property
It's unclear what that would be testing. We don't generate multiple props at a time for example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it would be testing that this doesn't crash
{ | ||
return IsObjectInitializerNamedAssignmentIdentifier(node, out var unused); | ||
} | ||
=> IsObjectInitializerNamedAssignmentIdentifier(node, out var unused); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: discard
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure.
node.Parent.IsParentKind(SyntaxKind.SubpatternElement); | ||
|
||
public bool IsPropertySubpattern(SyntaxNode node) | ||
=> node.Kind() == SyntaxKind.PropertySubpattern; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this belong here even though it's specific to C#?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SyntaxFacts is a superset of both languages. Either language can simply return 'false' for anything they dont' support.
@@ -154,6 +154,17 @@ protected AbstractGenerateMemberService() | |||
isStatic = false; | |||
return; | |||
} | |||
else if (syntaxFacts.IsNameOfSubpatternElement(expression) && | |||
syntaxFacts.IsPropertySubpattern(expression.Parent.Parent.Parent)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
expression.Parent.Parent.Parent
is really not clear here... if you want to think of syntaxFacts as language agnostic, this kind of check doesn't make sense in this place. Consider adding an out argument for subpatternElement
to IsNameOfSubpatternElement
so you could only do one Parent check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure.
} | ||
|
||
private static bool DecomposeBinaryOrAssignmentExpression(ExpressionSyntax expression, out SyntaxToken operatorToken, out ExpressionSyntax left, out ExpressionSyntax right) | ||
private static bool DecomposeBinaryOrAssignmentExpression(SyntaxNode node, out SyntaxToken operatorToken, out ExpressionSyntax left, out ExpressionSyntax right) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why was this changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the type inferred now works across expressions and patterns uniformly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see how that's relevant here. This method is called DecomposeBinaryOrAssignmentExpression and still only handles expressions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry, i thought you were asking a general question about the use of Expression vs SyntaxNode. I can make this ExpressionSyntax, though then i still have to add casts. That's not a big deal, just something that wasn't necessary if this was typed as SyntaxNode.
if (expression != null) | ||
{ | ||
expression = expression.WalkUpParentheses(); | ||
node = expression; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: consider pattern matching: if (node is ExpressionSyntax expression) node = expression.WalkUpParentheses()
@@ -899,7 +909,7 @@ private int GetArgumentListIndex(AttributeArgumentListSyntax attributeArgumentLi | |||
return (tokenIndex + 1) / 2; | |||
} | |||
|
|||
private IEnumerable<TypeInferenceInfo> InferTypeInBinaryOrAssignmentExpression(ExpressionSyntax binop, SyntaxToken operatorToken, ExpressionSyntax left, ExpressionSyntax right, ExpressionSyntax expressionOpt = null, SyntaxToken? previousToken = null) | |||
private IEnumerable<TypeInferenceInfo> InferTypeInBinaryOrAssignmentExpression(SyntaxNode binop, SyntaxToken operatorToken, ExpressionSyntax left, ExpressionSyntax right, ExpressionSyntax expressionOpt = null, SyntaxToken? previousToken = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like it should still be ExpressionSyntax
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
true. fixed.
var memberTypes = | ||
parentTypes.SelectMany(ti => GetMemberReturnTypes(ti.InferredType, identifier.Identifier.ValueText)) | ||
.SelectAsArray(t => new TypeInferenceInfo(t)); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is all this logic necessary? doesn't the compiler return a symbol for X?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. Let me try that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hrm.. Doesn't work. Not sure if that's intentional or not. @gafter can you shed some light here? The scenario is the following code:
void M2()
{
object o = null;
if (o is Blah { X: { Y: int i } }) //<-- call semanticModel.GetSymbolInfo on 'X' here.
{
}
}
class Frob { }
class Blah
{
public Frob X;
}
If I call semanticModel.GetSymbolInfo(id) on the identifier node for 'X', i don't get anything back. Is that expected? Or should i get back the the field symbol declared inside Blah? My intuition is 'yes', but i know the compiler has some very particular views about types/symbols and whatnot in patterns.
Can you clarify the expected behavior here of hte semantic model? Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The semantic model is not implemented for this syntactic location yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gafter Is the expectation that it will be at some later point? I just want to know what i should expect to work in the future so i can write the feature code in the appropriate manner :)
For example, you explained in the past how patterns don't have types, so i know to not even expect to be able to use .GetTypeInfo on a pattern. But the rules about things like getting symbol info on these parts of a pattern are a bit less clear.
Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I intend that identifiers referenced within a pattern will have SymbolInfo working. I will work on that next (though I am out for about a week, so it will be more than a week before I have it working).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. I'll wait for that to be done.
} | ||
|
||
private ImmutableArray<ITypeSymbol> GetMemberReturnTypes(ITypeSymbol type, string memberName) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.
|
||
class Blah | ||
{ | ||
public Frob X; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a similar test wehre X is a property rather than field?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it would be good to have one to exercise both cases in GetMemberReturnTypes
We apologize, but we are closing this PR due to code drift. We regret letting this PR go so long without attention, but at this point the code base has changed too much for us to revisit older PRs. Going forward, we will manage PRs better under an SLA currently under draft in issue #26266 – we encourage you to add comments to that draft as you see fit. Although that SLA is still in draft form, we nevertheless want to move forward with the identification of older PRs to give contributors as much early notice as possible to review and update them. If you are interested in pursuing this PR, please reset it against the head of master and we will reopen it. Thank you! |
@jaredpar please reopen this PR. This is work that @gafter has asked me to do as part of the pattern-matching feature work.
This PR is intentionally against @gafter's feature branch as that's where all the pattern work is happening. Note that work has been made in this PR as of a week ago, please don't close the PRs that are still actively being worked on, thanks :D |
@CyrusNajmabadi it's apparently not just you. I can't re-open this PR either. I'm not sure what is going on here. |
@CyrusNajmabadi You need to create a branch generateVarPatterns at commit d9a8bf3 in your fork in order for this pull request to be reopened. |
@sharwell can you clarify that? I already have a branch called generateVarExpressions . So i can't create one. Should i create another branch with a different name? Should i delete the existing branch? A set of steps would be appreciated here. |
@jaredpar Can we put a hold on closing out old PRs until a set of reasonable steps can be provided to contributers on how to actually reopen things? Thanks! |
@CyrusNajmabadi this seems to be the only PR that was affected. I've re-opened several others with no ill effects. |
Perhaps this is because it's a feature branch? I also don't seem to be able to reopen: #10420 |
I could not figure out how to reopen this. So i've opened #26355 instead. |
Fixes #9090