-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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 a DeclareAsNullable fixer #26630
Add a DeclareAsNullable fixer #26630
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 |
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 partial? #Resolved
|
||
[Fact] | ||
[WorkItem(26628, "https://github.com/dotnet/roslyn/issues/26628")] | ||
public async Task FixPropertyDeclaration() |
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider adding a test with var
#Resolved
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 a test with a field #Resolved
@@ -162,6 +162,9 @@ | |||
<data name="Remove_Unnecessary_Cast" xml:space="preserve"> | |||
<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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why partial? #Resolved
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: 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this needs to account for local functions as well #Resolved
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this doesn't need to be internal? #Resolved
📝 I'm investigating a bug with FixAll on a method with multiple |
// warning CS8625: Cannot convert null literal to non-nullable reference or unconstrained type parameter. | ||
// warning CS8600: Converting null literal or possible null value to non-nullable type. | ||
get { return ImmutableArray.Create("CS8625", "CS8600"); } | ||
} |
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.
can just be FixableDiagnosticIds { get; } =
or =>
#Resolved
|
||
foreach (var diagnostic in diagnostics) | ||
{ | ||
var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); |
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.
diagnostic.Location.FindNode(...) #Resolved
} | ||
|
||
return Task.CompletedTask; | ||
} | ||
|
||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please keep static #Resolved
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 had the hashset as instance field for a while. I'll restore static
. #Resolved
} | ||
} | ||
|
||
|
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: extra blank line #Resolved
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.
changes
var fixedDeclaration = SyntaxFactory.NullableType(declarationTypeToFix); | ||
editor.ReplaceNode(declarationTypeToFix, fixedDeclaration); | ||
alreadyHandled.Add(declarationTypeToFix); | ||
} |
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can just do . && alreadyHandled.Add(declarationType)
#Resolved
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the status on VB and nullable? happening or not? #Resolved
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if the containing member is a local function? #Resolved
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.
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jcouv I'm sorry to bother you about that :-/ #Resolved
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about local functions? #Resolved
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.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❗️ 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❕ Also need AnonymousMethodExpression
#Resolved
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 catch. 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.
I think you could use FirstOrDefault(a => a.IsReturnableConstruct())
(in SyntaxNodeExtensions)
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.
also, isn't there a method like GetAncestorOrThis
or FirstAncestorOrSelf
that takes a predicate somewhere?
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 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. |
@@ -80,8 +80,7 @@ private static TypeSyntax TryGetDeclarationTypeToFix(SyntaxNode node) | |||
|
|||
if (node.IsParentKind(SyntaxKind.ReturnStatement)) | |||
{ | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does that work properly for accessors/properties?
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this makes me think, what about indexers? they should probably be handled too right?
@@ -100,6 +100,17 @@ public static bool IsKind(this SyntaxNode node, SyntaxKind kind1, SyntaxKind kin | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Surely, nobody will ever want more than 11, right? ;-)
var containingMember = node.GetAncestors().FirstOrDefault(a => a.IsKind( | ||
SyntaxKind.MethodDeclaration, SyntaxKind.PropertyDeclaration, SyntaxKind.ParenthesizedLambdaExpression, SyntaxKind.SimpleLambdaExpression, | ||
SyntaxKind.LocalFunctionStatement, SyntaxKind.AnonymousMethodExpression, SyntaxKind.ConstructorDeclaration, SyntaxKind.DestructorDeclaration, | ||
SyntaxKind.OperatorDeclaration, SyntaxKind.IndexerDeclaration, SyntaxKind.EventDeclaration)); | ||
|
||
if (containingMember == 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.
nit: this is now unnecessary with pattern matching
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, right. That one (below) is fine :-)
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 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; |
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 wrapping
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 fornull
default value for non-nullable parameter should warn #26626)string x { get; set; } = null;
(depends on a compiler fix fornull
initializer 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.