Add a DeclareAsNullable fixer#26630
Add a DeclareAsNullable fixer#26630jcouv merged 11 commits intodotnet:features/NullableReferenceTypesfrom
Conversation
This is the one I'm most apprehensive about. When you compare the compiler code updates to the IDE code updates required for nullable-aware code, #23281 becomes painfully apparent. |
| namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics.DeclareAsNullable | ||
| { | ||
| [Trait(Traits.Feature, Traits.Features.CodeActionsDeclareAsNullable)] | ||
| public partial class DeclareAsNullableCodeFixTests : AbstractCSharpDiagnosticProviderBasedUserDiagnosticTest |
|
|
||
| [Fact] | ||
| [WorkItem(26628, "https://github.com/dotnet/roslyn/issues/26628")] | ||
| public async Task FixPropertyDeclaration() |
There was a problem hiding this comment.
I think a better approach might be to change the test to the actual desired behavior and add Skip with issue link. I think WorkItem is used for bugs that have been fixed (not for things that still need attention), isn't that the case? #Resolved
There was a problem hiding this comment.
I'll change, but WorkItem is fine. What matters is there is a tracking issue to drive fixing the problem :-) #Resolved
| { | ||
| static void M(string x = [|null|]) { } | ||
| }", parameters: s_nullableFeature); | ||
| } |
There was a problem hiding this comment.
consider adding a test with var #Resolved
There was a problem hiding this comment.
consider adding a test with a field #Resolved
| <value>Remove Unnecessary Cast</value> | ||
| </data> | ||
| <data name="Declare_as_Nullable" xml:space="preserve"> | ||
| <value>Declare as Nullable</value> |
There was a problem hiding this comment.
unrelated: I see the casing is quite inconsistent between features. For example "Remove Unnecessary Cast" above and "Cast is redundant" below. #Resolved
There was a problem hiding this comment.
I changed to lower case. I think it looks nicer, and the other strings below give me an excuse ;-)
In reply to: 186184879 [](ancestors = 186184879)
| namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.DeclareAsNullable | ||
| { | ||
| [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.DeclareAsNullable), Shared] | ||
| internal partial class DeclareAsNullableCodeFixProvider : SyntaxEditorBasedCodeFixProvider |
There was a problem hiding this comment.
nit: looking at the names of other code fixes, should this be called CSharpDeclareAsNullableCodeFixProvider? #Resolved
|
|
||
| foreach (var diagnostic in diagnostics) | ||
| { | ||
| var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); |
There was a problem hiding this comment.
is it possible that after 1 fix is applied, this location for another diagnostic would be outdated and therefore we might not find the correct node? sorry, I see we're looking at the original root #Resolved
|
|
||
| private static TypeSyntax TryGetDeclarationTypeToFix(SyntaxNode node) | ||
| { | ||
| if (!node.IsKind(SyntaxKind.NullLiteralExpression)) |
There was a problem hiding this comment.
should this be supported for default literals as well? or any expression that has a constant value of null if that's supported by the compiler? #Resolved
There was a problem hiding this comment.
I'm going to reserve judgement on that one for now. I want to address the cases that are very clear first, and gain experience from dogfooding. We can add later if valuable.
In reply to: 186186557 [](ancestors = 186186557)
| // return null; | ||
| if (node.IsParentKind(SyntaxKind.ReturnStatement)) | ||
| { | ||
| var methodDeclaration = node.GetAncestors<MethodDeclarationSyntax>().FirstOrDefault(); |
There was a problem hiding this comment.
this needs to account for local functions as well #Resolved
There was a problem hiding this comment.
Added a skipped test and filed a compiler bug for local functions.
In reply to: 186186732 [](ancestors = 186186732)
| if (node.Parent?.Parent?.IsParentKind(SyntaxKind.VariableDeclaration) == true) | ||
| { | ||
| var variableDeclaration = (VariableDeclarationSyntax)node.Parent.Parent.Parent; | ||
| if (variableDeclaration.Variables.Count != 1) |
There was a problem hiding this comment.
note: there are probably other cases that need to be supported, such as a for each statement variable. if you don't implement this, please create an issue so that somebody can fix this later I'm probably wrong. I don't see how this could come up in a for each statement. Are we sure there are no other cases though? #Resolved
There was a problem hiding this comment.
I can't think of any other scenario at the moment. But we can always fill any gaps we discover. I'll be testing this on Roslyn codebase, so that may give some ideas. #Resolved
| if (node.Parent?.IsParentKind(SyntaxKind.PropertyDeclaration) == true) | ||
| { | ||
| var propertyDeclaration = (PropertyDeclarationSyntax)node.Parent.Parent; | ||
| return propertyDeclaration.Type; |
There was a problem hiding this comment.
we might also want to offer this inside a property getter or setter:
string X
{
get { return null; }
}
``` #Resolved| // static string M() => null; | ||
| if (node.IsParentKind(SyntaxKind.ArrowExpressionClause) && node.Parent.IsParentKind(SyntaxKind.MethodDeclaration)) | ||
| { | ||
| var arrowMethod = (MethodDeclarationSyntax)node.Parent.Parent; |
There was a problem hiding this comment.
same comment about local functions and maybe properties #Resolved
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| internal static void MakeDeclarationNullable(Document document, SyntaxEditor editor, SyntaxNode node) |
There was a problem hiding this comment.
I think this doesn't need to be internal? #Resolved
|
📝 I'm investigating a bug with FixAll on a method with multiple |
There was a problem hiding this comment.
can just be FixableDiagnosticIds { get; } = or =>
#Resolved
There was a problem hiding this comment.
diagnostic.Location.FindNode(...) #Resolved
| } | ||
|
|
||
| internal static void MakeDeclarationNullable(Document document, SyntaxEditor editor, SyntaxNode node) | ||
| private void MakeDeclarationNullable(Document document, SyntaxEditor editor, SyntaxNode node, HashSet<TypeSyntax> alreadyHandled) |
There was a problem hiding this comment.
please keep static #Resolved
There was a problem hiding this comment.
I had the hashset as instance field for a while. I'll restore static. #Resolved
| } | ||
| } | ||
|
|
||
|
|
There was a problem hiding this comment.
nit: extra blank line #Resolved
| var fixedDeclaration = SyntaxFactory.NullableType(declarationTypeToFix); | ||
| editor.ReplaceNode(declarationTypeToFix, fixedDeclaration); | ||
| alreadyHandled.Add(declarationTypeToFix); | ||
| } |
There was a problem hiding this comment.
this will probably do the wrong with wrt trivia. we should do SyntaxFactory.NullableType(declarationTypeToFix.WithoutTrivia()).WithTriviaFrom(declarationType); #Resolved
There was a problem hiding this comment.
can just do . && alreadyHandled.Add(declarationType) #Resolved
There was a problem hiding this comment.
Can you have trivia tests? #Resolved
| var root = editor.OriginalRoot; | ||
|
|
||
| // a method can have multiple `return null;` statements, but we should only fix its return type once | ||
| var alreadyHandled = new HashSet<TypeSyntax>(); |
There was a problem hiding this comment.
consider pooling this. though ti's just one doc, so likely not too bad. #Resolved
| namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.DeclareAsNullable | ||
| { | ||
| [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.DeclareAsNullable), Shared] | ||
| internal class CSharpDeclareAsNullableCodeFixProvider : SyntaxEditorBasedCodeFixProvider |
There was a problem hiding this comment.
What's the status on VB and nullable? happening or not? #Resolved
There was a problem hiding this comment.
Haven't heard any update. We're trying to wrap up nullable for C# first.
|
|
||
| if (node.IsParentKind(SyntaxKind.ReturnStatement)) | ||
| { | ||
| var containingMember = node.GetAncestors().FirstOrDefault(a => a.IsKind(SyntaxKind.MethodDeclaration, SyntaxKind.PropertyDeclaration)); |
There was a problem hiding this comment.
what if the containing member is a local function? #Resolved
There was a problem hiding this comment.
There was a problem hiding this comment.
Note: i would not add skipped tests for this. Just add the tests with the current bahvior and the note that this will change when the corresponding bug is fixed. That way they do break and are appropriately updated when the change is made . Otherwise, they can just sit there skipped with people not noticing for years. #Resolved
There was a problem hiding this comment.
@CyrusNajmabadi I'm sorry, that is kind of my fault if you look at the feedback above 😄 #Resolved
There was a problem hiding this comment.
@jcouv I'm sorry to bother you about that :-/ #Resolved
There was a problem hiding this comment.
My reasoning was that regular tests with WorkAttribute would go unnoticed without raising any eyebrows as opposed to skipped tests. Sorry again you had to change this twice. #Resolved
There was a problem hiding this comment.
No worries. Both rationales offer some benefits. Cyrus's argument (easier to forget skipped tests) carried the day ;-) #Resolved
| } | ||
|
|
||
| // void M(string x = null) { } | ||
| if (node.Parent?.IsParentKind(SyntaxKind.Parameter) == true) |
There was a problem hiding this comment.
no need for ? before IsParentKind, the Is helpers do the right thing on null (they are trying to emulate the 'is' language feature). #Resolved
| } | ||
|
|
||
| // static string M() => null; | ||
| if (node.IsParentKind(SyntaxKind.ArrowExpressionClause) && node.Parent.IsParentKind(SyntaxKind.MethodDeclaration)) |
There was a problem hiding this comment.
what about local functions? #Resolved
There was a problem hiding this comment.
@CyrusNajmabadi I have asked about that before. @jcouv created an issue to track this: #26639 #Resolved
|
The purpose for this fixer was just to handle the trivial cases, that are not controversial, and can be used as FixAll. I'll start a second fixer that will be meant for individual fixes (no FixAll). For instance, you have Beyond that, I'm open to suggestions, but I expect the dogfooding experience will give more ideas ;-) |
|
@CyrusNajmabadi 304 changes is only for those trivially recognizable warnings in the CodeAnalysis project alone (I didn't look at CSharpCodeAnalysis or IDE code). There are ~1500 warnings left to fix beyond that, in CodeAnalysis ;-) #Resolved |
Ohhhh, that makes a lot more sense. #Resolved |
|
@dotnet/roslyn-ide We'd like to include this in the prototype we'll share at BUILD tomorrow. The feedback from Cyrus and Neme has been addressed. Please review. #Resolved |
|
|
||
| if (node.IsParentKind(SyntaxKind.ReturnStatement)) | ||
| { | ||
| var containingMember = node.GetAncestors().FirstOrDefault(a => a.IsKind(SyntaxKind.MethodDeclaration, SyntaxKind.PropertyDeclaration)); |
There was a problem hiding this comment.
❗️ It's fine to not support anonymous/local functions, but you need to at least detect the case and not try to modify the return value of an unrelated method. Since this is intended for Fix All scenarios, users will end up changing values that don't need to change and in the transitive closure it will be a mess. The resolution is to add local functions and the various lambda syntaxes to this upward search, and for anything not supported return without making changes. #Resolved
There was a problem hiding this comment.
The compiler doesn't produce the triggering warnings in such cases. But I do expect it will (soon) in the case of local functions. I have a test that guards that, and also added defensive code just in case.
#Resolved
| if (node.IsParentKind(SyntaxKind.ReturnStatement)) | ||
| { | ||
| var containingMember = node.GetAncestors().FirstOrDefault(a => a.IsKind(SyntaxKind.MethodDeclaration, SyntaxKind.PropertyDeclaration, | ||
| SyntaxKind.ParenthesizedLambdaExpression, SyntaxKind.SimpleLambdaExpression, SyntaxKind.LocalFunctionStatement)); |
There was a problem hiding this comment.
❕ Also need AnonymousMethodExpression #Resolved
There was a problem hiding this comment.
I think you could use FirstOrDefault(a => a.IsReturnableConstruct()) (in SyntaxNodeExtensions)
There was a problem hiding this comment.
also, isn't there a method like GetAncestorOrThis or FirstAncestorOrSelf that takes a predicate somewhere?
There was a problem hiding this comment.
IsReturnableConstruct
Even better. Thanks!
isn't there a method like ... that takes a predicate somewhere?
Didn't find one. I'll leave as-is.
There was a problem hiding this comment.
Actually... that might be wrong. IsReturnableConstruct considers individual accessors whereas you're looking at the actual property. I don't think this will work. Sorry :/
| // string x { get { return null; } } | ||
| return ((PropertyDeclarationSyntax)containingMember).Type; | ||
|
|
||
| case SyntaxKind.ParenthesizedLambdaExpression: |
There was a problem hiding this comment.
💡 May as well use default: #Resolved
@jcouv Do we have a tracking issue to handle the remaining case? |
It's a fairly fluid situation at this point. The common cases are being handled as they are observed at scale, and we can think about how to handle the remaining cases once the vision for the underlying feature becomes clearer. The New Language Feature - Nullable Reference Types label is probably the best way to track related issues. |
| { | ||
| var containingMember = node.GetAncestors().FirstOrDefault(a => a.IsKind(SyntaxKind.MethodDeclaration, SyntaxKind.PropertyDeclaration, | ||
| SyntaxKind.ParenthesizedLambdaExpression, SyntaxKind.SimpleLambdaExpression, SyntaxKind.LocalFunctionStatement, SyntaxKind.AnonymousMethodExpression)); | ||
| var containingMember = node.GetAncestors().FirstOrDefault(a => a.IsReturnableConstruct()); |
There was a problem hiding this comment.
Does that work properly for accessors/properties?
There was a problem hiding this comment.
Hum, good catch. A test did regress. I'll adjust.
|
In addition to what Sam said, we're also tracking many work items in the nullable umbrella issue. As far as how to improve the nullable fixer, I'm dogfooding the nullability preview on Roslyn, and using that experience to give me ideas of what can be automated. |
| return ((PropertyDeclarationSyntax)containingMember).Type; | ||
|
|
||
| default: | ||
| return null; |
There was a problem hiding this comment.
you could go back and remove IsReturnableConstruct or I would persnally keep it and use pattern matching here to get the property from the accessor we found:
switch (containingMember)
{
case MethodDeclarationSyntax method:
...
case AccessorDeclarationSyntax accessor when accessor.Parent is PropertyDeclarationSyntax property:
...
}There was a problem hiding this comment.
this makes me think, what about indexers? they should probably be handled too right?
| return csharpKind == kind1 || csharpKind == kind2 || csharpKind == kind3 || csharpKind == kind4 || csharpKind == kind5 || csharpKind == kind6; | ||
| } | ||
|
|
||
| public static bool IsKind(this SyntaxNode node, SyntaxKind kind1, SyntaxKind kind2, SyntaxKind kind3, SyntaxKind kind4, SyntaxKind kind5, SyntaxKind kind6, SyntaxKind kind7, SyntaxKind kind8, SyntaxKind kind9, SyntaxKind kind10, SyntaxKind kind11) |
There was a problem hiding this comment.
Surely, nobody will ever want more than 11, right? ;-)
| SyntaxKind.LocalFunctionStatement, SyntaxKind.AnonymousMethodExpression, SyntaxKind.ConstructorDeclaration, SyntaxKind.DestructorDeclaration, | ||
| SyntaxKind.OperatorDeclaration, SyntaxKind.IndexerDeclaration, SyntaxKind.EventDeclaration)); | ||
|
|
||
| if (containingMember == null) |
There was a problem hiding this comment.
nit: this is now unnecessary with pattern matching
There was a problem hiding this comment.
I don't want to use pattern matching here because it involves many type tests, repeated for each level of ancestry.
There was a problem hiding this comment.
I meant the pattern matching that you're already using below. Not that you should use it above as well.
There was a problem hiding this comment.
it involves many type tests, repeated for each level of ancestry
Is there a performance problem with that? I've seen GetAncestorOrSelf<T> and such used all over the place.
There was a problem hiding this comment.
Oh, right. That one (below) is fine :-)
There was a problem hiding this comment.
Yes, I suspect there would be some performance impact. My prior could be changed with some micro-benchmark, once someone takes the time to check.
The problem is not with GetAncestor method itself. But it's a loop, and you want to make iterations cheaper when in such a tight loop.
| } | ||
|
|
||
| var csharpKind = node.Kind(); | ||
| return csharpKind == kind1 || csharpKind == kind2 || csharpKind == kind3 || csharpKind == kind4 || csharpKind == kind5 || csharpKind == kind6 || csharpKind == kind7 || csharpKind == kind8 || csharpKind == kind9 || csharpKind == kind10 || csharpKind == kind11; |
This fixer is designed to automatically fix only some trivial cases (so that FixAll is appropriate):
string x = null;(note: multi-declarations don't get fixed)string M() { return null }andstring M() => null;void M(string x = null) ...(depends on a compiler fix fornulldefault value for non-nullable parameter should warn #26626)string x { get; set; } = null;(depends on a compiler fix fornullinitializer for non-nullable property (or field) should warn #26628),string x => null;This is to support customers migrating their projects to use nullable annotations, including our own dogfooding.
@dotnet/roslyn-ide for review.
@dotnet/roslyn-compiler FYI.