Skip to content
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

Implement Analyzer and best-effort Code Fix for SafeHandle Public Parameterless Constructor guidance. #5043

Merged
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
CA1418 | Interoperability | Warning | UseValidPlatformString, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1418)
CA1419 | Interoperability | Info | ProvidePublicParameterlessSafeHandleConstructor, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1419)
CA1839 | Performance | Info | UseEnvironmentMembers, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1839)
CA1840 | Performance | Info | UseEnvironmentMembers, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1840)
CA1841 | Performance | Info | PreferDictionaryContainsMethods, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1841)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;

namespace Microsoft.NetCore.Analyzers.InteropServices
{
[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared]
public sealed class ProvidePublicParameterlessSafeHandleConstructorFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(ProvidePublicParameterlessSafeHandleConstructorAnalyzer.RuleId);

public sealed override FixAllProvider GetFixAllProvider()
{
// See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
return WellKnownFixAllProviders.BatchFixer;
}

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
SyntaxGenerator generator = SyntaxGenerator.GetGenerator(context.Document);
SyntaxNode root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

SyntaxNode enclosingNode = root.FindNode(context.Span);
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to pass true for getInnermostNodeForTie? If so, we should add a test too.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't know what that flag does, so I don't know if I need to pass it or what a test case would look like.

Copy link
Member

Choose a reason for hiding this comment

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

@jkoritzinsky The flag is when two nodes have the same span, which one to get?
I don't think this can happen for the case of this analyzer, but will let @mavasani confirm in case there is any corner case I'm missing.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't believe that can happen with the nodes we're looking at in this analyzer, so I don't think we need to worry about it.

Copy link
Member

Choose a reason for hiding this comment

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

@mavasani I'm quite confused regarding CodeFixContext when it has multiple diagnostics, which diagnostic will the value of context.Span belong to?

Copy link
Contributor

Choose a reason for hiding this comment

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

All diagnostics in the code fix context should have the same ID and span.

SyntaxNode declaration = generator.GetDeclaration(enclosingNode);
if (declaration == null)
{
return;
}

Diagnostic diagnostic = context.Diagnostics.First();
if (diagnostic.Properties.ContainsKey(ProvidePublicParameterlessSafeHandleConstructorAnalyzer.DiagnosticPropertyConstructorExists))
{
context.RegisterCodeFix(
new MyCodeAction(
MicrosoftNetCoreAnalyzersResources.MakeParameterlessConstructorPublic,
async ct => await MakeParameterlessConstructorPublic(declaration, context.Document, context.CancellationToken).ConfigureAwait(false),
equivalenceKey: nameof(MicrosoftNetCoreAnalyzersResources.MakeParameterlessConstructorPublic)),
diagnostic);
}
else if (diagnostic.Properties.ContainsKey(ProvidePublicParameterlessSafeHandleConstructorAnalyzer.DiagnosticPropertyBaseConstructorAccessible))
mavasani marked this conversation as resolved.
Show resolved Hide resolved
{
context.RegisterCodeFix(
new MyCodeAction(
MicrosoftNetCoreAnalyzersResources.AddPublicParameterlessConstructor,
async ct => await AddParameterlessConstructor(declaration, context.Document, context.CancellationToken).ConfigureAwait(false),
equivalenceKey: nameof(MicrosoftNetCoreAnalyzersResources.AddPublicParameterlessConstructor)),
diagnostic);
}
}

private static async Task<Document> AddParameterlessConstructor(SyntaxNode declaration, Document document, CancellationToken ct)
{
var editor = await DocumentEditor.CreateAsync(document, ct).ConfigureAwait(false);
INamedTypeSymbol type = (INamedTypeSymbol)editor.SemanticModel.GetDeclaredSymbol(declaration, ct);
var generator = editor.Generator;

var parameterlessConstructor = generator.ConstructorDeclaration(type.Name, accessibility: Accessibility.Public);

editor.AddMember(declaration, parameterlessConstructor);

return editor.GetChangedDocument();
}

private static async Task<Document> MakeParameterlessConstructorPublic(SyntaxNode declaration, Document document, CancellationToken ct)
{
var editor = await DocumentEditor.CreateAsync(document, ct).ConfigureAwait(false);
editor.SetAccessibility(declaration, Accessibility.Public);
return editor.GetChangedDocument();
}

private class MyCodeAction : DocumentChangeAction
{
public MyCodeAction(string title, Func<CancellationToken, Task<Document>> createChangedDocument, string equivalenceKey)
: base(title, createChangedDocument, equivalenceKey)
{
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Immutable;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.NetCore.Analyzers.InteropServices
{
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class ProvidePublicParameterlessSafeHandleConstructorAnalyzer : DiagnosticAnalyzer
{
internal const string RuleId = "CA1419";

internal const string DiagnosticPropertyConstructorExists = "ConstructorExists";
internal const string DiagnosticPropertyBaseConstructorAccessible = "BaseConstructorAccessible";

private static readonly LocalizableString s_localizableTitle = new LocalizableResourceString(nameof(MicrosoftNetCoreAnalyzersResources.ProvidePublicParameterlessSafeHandleConstructorTitle), MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources));
private static readonly LocalizableString s_localizableMessage = new LocalizableResourceString(nameof(MicrosoftNetCoreAnalyzersResources.ProvidePublicParameterlessSafeHandleConstructorMessage), MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources));
private static readonly LocalizableString s_localizableDescription = new LocalizableResourceString(nameof(MicrosoftNetCoreAnalyzersResources.ProvidePublicParameterlessSafeHandleConstructorDescription), MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources));
internal static DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
RuleId,
s_localizableTitle,
s_localizableMessage,
DiagnosticCategory.Interoperability,
RuleLevel.IdeSuggestion,
description: s_localizableDescription,
isPortedFxCopRule: false,
isDataflowRule: false);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(
context =>
{
if (context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeInteropServicesSafeHandle, out var safeHandleType))
{
context.RegisterSymbolAction(context => AnalyzeSymbol(context, safeHandleType), SymbolKind.NamedType);
}
});
}

private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbol safeHandleType)
{
INamedTypeSymbol type = (INamedTypeSymbol)context.Symbol;

if (type.TypeKind != TypeKind.Class)
{
// SafeHandle-derived types can only be classes.
return;
}

if (type.IsAbstract || !type.Inherits(safeHandleType))
{
// We only want to put the diagnostic on concrete SafeHandle-derived types.
return;
}

ImmutableDictionary<string, string?> diagnosticPropertyBag = ImmutableDictionary<string, string?>.Empty;
ISymbol targetWarningSymbol = type;
foreach (var constructor in type.InstanceConstructors)
{
if (constructor.Parameters.Length == 0)
{
if (constructor.DeclaredAccessibility == Accessibility.Public)
{
// The parameterless constructor is public, so there is no diagnostic to emit.
return;
}

// If a parameterless constructor has been defined, emit the diagnostic on the constructor instead of on the type.
targetWarningSymbol = constructor;
diagnosticPropertyBag = diagnosticPropertyBag.Add(DiagnosticPropertyConstructorExists, null);
jkoritzinsky marked this conversation as resolved.
Show resolved Hide resolved
break;
}
}

foreach (var constructor in type.BaseType.InstanceConstructors)
{
if (constructor.Parameters.Length == 0 &&
context.Compilation.IsSymbolAccessibleWithin(constructor, type))
{
diagnosticPropertyBag = diagnosticPropertyBag.Add(DiagnosticPropertyBaseConstructorAccessible, null);
break;
}
}

context.ReportDiagnostic(targetWarningSymbol.CreateDiagnostic(Rule, diagnosticPropertyBag, type.Name));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1611,4 +1611,19 @@
<data name="DoNotUseWhenAllWithSingleTaskFix" xml:space="preserve">
<value>Replace 'WhenAll' call with argument</value>
</data>
<data name="ProvidePublicParameterlessSafeHandleConstructorDescription" xml:space="preserve">
<value>Providing a public parameterless constructor for a type derived from 'System.Runtime.InteropServices.SafeHandle' enables better performance and usage with source-generated interop solutions.</value>
</data>
<data name="ProvidePublicParameterlessSafeHandleConstructorMessage" xml:space="preserve">
<value>Provide a public parameterless constructor for the '{0}' type that is derived from 'System.Runtime.InteropServices.SafeHandle'</value>
</data>
<data name="ProvidePublicParameterlessSafeHandleConstructorTitle" xml:space="preserve">
<value>Provide a public parameterless constructor for concrete types derived from 'System.Runtime.InteropServices.SafeHandle'</value>
</data>
<data name="AddPublicParameterlessConstructor" xml:space="preserve">
<value>Add a public constructor that takes zero parameters</value>
</data>
<data name="MakeParameterlessConstructorPublic" xml:space="preserve">
<value>Make the constructor that takes zero parameters 'public'</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
<target state="translated">Přidejte k tomuto poli atribut NonSerialized</target>
<note />
</trans-unit>
<trans-unit id="AddPublicParameterlessConstructor">
<source>Add a public constructor that takes zero parameters</source>
<target state="new">Add a public constructor that takes zero parameters</target>
<note />
</trans-unit>
<trans-unit id="AddSerializableAttributeCodeActionTitle">
<source>Add Serializable attribute</source>
<target state="translated">Přidat atribut Serializable</target>
Expand Down Expand Up @@ -1307,6 +1312,11 @@
<target state="translated">Nepoužívat nezabezpečený deserializátor LosFormatter</target>
<note />
</trans-unit>
<trans-unit id="MakeParameterlessConstructorPublic">
<source>Make the constructor that takes zero parameters 'public'</source>
<target state="new">Make the constructor that takes zero parameters 'public'</target>
<note />
</trans-unit>
<trans-unit id="MarkAllNonSerializableFieldsDescription">
<source>An instance field of a type that is not serializable is declared in a type that is serializable.</source>
<target state="translated">Pole instance typu, který se nedá serializovat, je deklarované v typu, který se serializovat dá.</target>
Expand Down Expand Up @@ -1787,6 +1797,21 @@
<target state="translated">Poskytujte metody deserializace pro volitelné pole</target>
<note />
</trans-unit>
<trans-unit id="ProvidePublicParameterlessSafeHandleConstructorDescription">
<source>Providing a public parameterless constructor for a type derived from 'System.Runtime.InteropServices.SafeHandle' enables better performance and usage with source-generated interop solutions.</source>
<target state="new">Providing a public parameterless constructor for a type derived from 'System.Runtime.InteropServices.SafeHandle' enables better performance and usage with source-generated interop solutions.</target>
<note />
</trans-unit>
<trans-unit id="ProvidePublicParameterlessSafeHandleConstructorMessage">
<source>Provide a public parameterless constructor for the '{0}' type that is derived from 'System.Runtime.InteropServices.SafeHandle'</source>
<target state="new">Provide a public parameterless constructor for the '{0}' type that is derived from 'System.Runtime.InteropServices.SafeHandle'</target>
<note />
</trans-unit>
<trans-unit id="ProvidePublicParameterlessSafeHandleConstructorTitle">
<source>Provide a public parameterless constructor for concrete types derived from 'System.Runtime.InteropServices.SafeHandle'</source>
<target state="new">Provide a public parameterless constructor for concrete types derived from 'System.Runtime.InteropServices.SafeHandle'</target>
<note />
</trans-unit>
<trans-unit id="ProvideStreamMemoryBasedAsyncOverridesDescription">
<source>To improve performance, override the memory-based async methods when subclassing 'Stream'. Then implement the array-based methods in terms of the memory-based methods.</source>
<target state="new">To improve performance, override the memory-based async methods when subclassing 'Stream'. Then implement the array-based methods in terms of the memory-based methods.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
<target state="translated">Fügen Sie diesem Feld das Attribut "NonSerialized" hinzu.</target>
<note />
</trans-unit>
<trans-unit id="AddPublicParameterlessConstructor">
<source>Add a public constructor that takes zero parameters</source>
<target state="new">Add a public constructor that takes zero parameters</target>
<note />
</trans-unit>
<trans-unit id="AddSerializableAttributeCodeActionTitle">
<source>Add Serializable attribute</source>
<target state="translated">Serializable-Attribut hinzufügen</target>
Expand Down Expand Up @@ -1307,6 +1312,11 @@
<target state="translated">Nicht den unsicheren LosFormatter zur Deserialisierung verwenden</target>
<note />
</trans-unit>
<trans-unit id="MakeParameterlessConstructorPublic">
<source>Make the constructor that takes zero parameters 'public'</source>
<target state="new">Make the constructor that takes zero parameters 'public'</target>
<note />
</trans-unit>
<trans-unit id="MarkAllNonSerializableFieldsDescription">
<source>An instance field of a type that is not serializable is declared in a type that is serializable.</source>
<target state="translated">Ein Instanzfeld eines Typs, der nicht serialisierbar ist, wurde in einem Typ deklariert, der serialisierbar ist.</target>
Expand Down Expand Up @@ -1787,6 +1797,21 @@
<target state="translated">Deserialisierungsmethoden für optionale Felder angeben</target>
<note />
</trans-unit>
<trans-unit id="ProvidePublicParameterlessSafeHandleConstructorDescription">
<source>Providing a public parameterless constructor for a type derived from 'System.Runtime.InteropServices.SafeHandle' enables better performance and usage with source-generated interop solutions.</source>
<target state="new">Providing a public parameterless constructor for a type derived from 'System.Runtime.InteropServices.SafeHandle' enables better performance and usage with source-generated interop solutions.</target>
<note />
</trans-unit>
<trans-unit id="ProvidePublicParameterlessSafeHandleConstructorMessage">
<source>Provide a public parameterless constructor for the '{0}' type that is derived from 'System.Runtime.InteropServices.SafeHandle'</source>
<target state="new">Provide a public parameterless constructor for the '{0}' type that is derived from 'System.Runtime.InteropServices.SafeHandle'</target>
<note />
</trans-unit>
<trans-unit id="ProvidePublicParameterlessSafeHandleConstructorTitle">
<source>Provide a public parameterless constructor for concrete types derived from 'System.Runtime.InteropServices.SafeHandle'</source>
<target state="new">Provide a public parameterless constructor for concrete types derived from 'System.Runtime.InteropServices.SafeHandle'</target>
<note />
</trans-unit>
<trans-unit id="ProvideStreamMemoryBasedAsyncOverridesDescription">
<source>To improve performance, override the memory-based async methods when subclassing 'Stream'. Then implement the array-based methods in terms of the memory-based methods.</source>
<target state="new">To improve performance, override the memory-based async methods when subclassing 'Stream'. Then implement the array-based methods in terms of the memory-based methods.</target>
Expand Down
Loading