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 DisableRuntimeMarshalling analyzer and code fixer #5822

Merged
merged 22 commits into from
Mar 18, 2022
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ac9e294
Implement DisableRuntimeMarshalling analyzer and partial code fixer.
jkoritzinsky Jan 28, 2022
27cc464
Implement custom FixAllProvider and implement the code fix for non-nu…
jkoritzinsky Jan 28, 2022
3129529
Implement nullable PtrToStructure code fix
jkoritzinsky Jan 28, 2022
2584bab
Update src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNet…
jkoritzinsky Jan 28, 2022
16dafc4
Merge branch 'main' of github.com:dotnet/roslyn-analyzers into disabl…
jkoritzinsky Jan 29, 2022
276efc7
Merge branch 'disableruntimemarshalling-analyzer' of github.com:jkori…
jkoritzinsky Jan 29, 2022
988816f
Get the build clean locally.
jkoritzinsky Jan 29, 2022
329a8b2
Use a RWLock to make the auto-layout cache thread-safe
jkoritzinsky Jan 31, 2022
fb177a5
Update tests/analyzers that were failing with the new Roslyn that we'…
jkoritzinsky Jan 31, 2022
217a5de
Merge branch 'main' into disableruntimemarshalling-analyzer
jkoritzinsky Feb 2, 2022
b02f9f8
Merge branch 'main' into disableruntimemarshalling-analyzer
jkoritzinsky Feb 4, 2022
ae305aa
Build with the same version of Roslyn that we're testing against. Use…
jkoritzinsky Feb 9, 2022
eed4ea1
Don't update the Roslyn we build with. I don't want to deal with the …
jkoritzinsky Feb 9, 2022
03e7124
Need IArgumentOperation.Value to get the value.
jkoritzinsky Feb 9, 2022
21ca8a3
Add some VB tests for the analyzer
jkoritzinsky Mar 8, 2022
dd5745c
PR feedback other than resources
jkoritzinsky Mar 8, 2022
aef7052
PR feedback
jkoritzinsky Mar 15, 2022
93043c9
Merge branch 'main' of github.com:dotnet/roslyn-analyzers into disabl…
jkoritzinsky Mar 15, 2022
24a8136
Use MarkupOptions.UseFirstDescriptor to continue using the diagnostic…
jkoritzinsky Mar 15, 2022
8a29a96
PR feedback and add tests for local functions
jkoritzinsky Mar 18, 2022
0cc8679
Add some tests of sadness
jkoritzinsky Mar 18, 2022
e1cd121
Fix test failures
jkoritzinsky Mar 18, 2022
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 NuGet.config
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<packageSources>
<clear />
<add key="dotnet5" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json" />
<add key="dotnet7" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7/nuget/v3/index.json" />
<add key="dotnet-eng" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" />
<add key="dotnet-tools" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" />
<add key="dotnet-public" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" />
Expand Down
2 changes: 1 addition & 1 deletion eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
<!-- Roslyn -->
<MicrosoftCodeAnalysisVersion>3.3.1</MicrosoftCodeAnalysisVersion>
<MicrosoftCodeAnalysisForRoslynDiagnosticsAnalyzersVersion>3.7.0</MicrosoftCodeAnalysisForRoslynDiagnosticsAnalyzersVersion>
<MicrosoftCodeAnalysisVersionForTests>4.0.1</MicrosoftCodeAnalysisVersionForTests>
<MicrosoftCodeAnalysisVersionForTests>4.1.0</MicrosoftCodeAnalysisVersionForTests>
<DogfoodAnalyzersVersion>3.3.3</DogfoodAnalyzersVersion>
<DogfoodNetAnalyzersVersion>5.0.4-preview1.21126.5</DogfoodNetAnalyzersVersion>
<MicrosoftCodeAnalysisBannedApiAnalyzersVersion>$(DogfoodAnalyzersVersion)</MicrosoftCodeAnalysisBannedApiAnalyzersVersion>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.InteropServices
{
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public sealed class CSharpDisableRuntimeMarshallingFixer : CodeFixProvider
Copy link
Contributor

Choose a reason for hiding this comment

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

I see that we mark the analyzer as supporting VB, but we don't have any VB code fix provider. Was this explicitly called out in the triage for this analyzer/fixer?

Copy link
Member Author

Choose a reason for hiding this comment

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

All of the code-fixes provide fixes that are not applicable to VB code. They all require C# unsafe language features that are not available in VB.

{
private class CustomFixAllProvider : DocumentBasedFixAllProvider
{
public static readonly CustomFixAllProvider Instance = new();

protected override string CodeActionTitle => MicrosoftNetCoreAnalyzersResources.UseDisabledMarshallingEquivalentCodeFix;

protected override async Task<SyntaxNode> FixAllInDocumentAsync(FixAllContext fixAllContext, Document document, ImmutableArray<Diagnostic> diagnostics)
{
if (document.Project.CompilationOptions is CSharpCompilationOptions { AllowUnsafe: false })
{
// We can't code fix if unsafe code isn't allowed.
return await document.GetSyntaxRootAsync(fixAllContext.CancellationToken);
}
Copy link
Member

Choose a reason for hiding this comment

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

Is this redundant? This was already checked in RegisterCodeFixesAsync

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 tried turning this into an assert and it tripped on a few tests, so I guess it's not redundant.

var editor = await DocumentEditor.CreateAsync(document, fixAllContext.CancellationToken).ConfigureAwait(false);
SyntaxNode root = await document.GetSyntaxRootAsync(fixAllContext.CancellationToken).ConfigureAwait(false);

Dictionary<IBlockOperation, IdentifierGenerator> scopeMap = new();
foreach (var diagnostic in diagnostics)
{
if (diagnostic.Properties[DisableRuntimeMarshallingAnalyzer.CanConvertToDisabledMarshallingEquivalentKey] is not null)
{
SyntaxNode node = root.FindNode(diagnostic.Location.SourceSpan);
IBlockOperation? block = editor.SemanticModel.GetOperation(node, fixAllContext.CancellationToken).GetFirstParentBlock();
IdentifierGenerator identifierGenerator;
if (block is null)
{
identifierGenerator = new IdentifierGenerator(editor.SemanticModel, node.SpanStart);
}
else if (!scopeMap.TryGetValue(block, out identifierGenerator))
{
identifierGenerator = scopeMap[block] = new IdentifierGenerator(editor.SemanticModel, block);
}
if (TryRewriteMethodCall(node, editor, identifierGenerator, addRenameAnnotation: false, fixAllContext.CancellationToken))
{
AddUnsafeModifierToEnclosingMethod(editor, node);
}
}
}
return editor.GetChangedRoot();
}
}
private class IdentifierGenerator
{
private int? _nextIdentifier;

public IdentifierGenerator(SemanticModel model, int offsetForSpeculativeSymbolResolution)
{
_nextIdentifier = FindFirstUnusedIdentifierIndex(model, offsetForSpeculativeSymbolResolution, "ptr");
}
public IdentifierGenerator(SemanticModel model, IBlockOperation block)
{
_nextIdentifier = FindFirstUnusedIdentifierIndex(model, block.Syntax.SpanStart, "ptr");
HashSet<string> localNames = new HashSet<string>(block.Locals.Select(x => x.Name));
string? identifier = NextIdentifier();
while (identifier is not null && localNames.Contains(identifier))
{
identifier = NextIdentifier();
}
if (identifier is not null)
{
// The last identifier was not in use, so go back one to use it the next call.
_nextIdentifier--;
}
}

public string? NextIdentifier()
{
if (_nextIdentifier == null || _nextIdentifier == int.MaxValue)
{
return null;
}

if (_nextIdentifier == 0)
{
_nextIdentifier++;
return "ptr";
}

return $"ptr{_nextIdentifier++}";
}
}

public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(DisableRuntimeMarshallingAnalyzer.MethodUsesRuntimeMarshallingEvenWhenMarshallingDisabledId);

public sealed override FixAllProvider GetFixAllProvider()
{
return CustomFixAllProvider.Instance;
}

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
jkoritzinsky marked this conversation as resolved.
Show resolved Hide resolved
{
if (context.Document.Project.CompilationOptions is CSharpCompilationOptions { AllowUnsafe: false })
{
// We can't code fix if unsafe code isn't allowed.
return;
}
SyntaxNode root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

SyntaxNode enclosingNode = root.FindNode(context.Span);

foreach (var diagnostic in context.Diagnostics)
{
if (diagnostic.Properties[DisableRuntimeMarshallingAnalyzer.CanConvertToDisabledMarshallingEquivalentKey] is not null)
{
context.RegisterCodeFix(
CodeAction.Create(
MicrosoftNetCoreAnalyzersResources.UseDisabledMarshallingEquivalentCodeFix,
async ct => await UseDisabledMarshallingEquivalentAsync(enclosingNode, context.Document, context.CancellationToken).ConfigureAwait(false),
equivalenceKey: nameof(MicrosoftNetCoreAnalyzersResources.UseDisabledMarshallingEquivalentCodeFix)),
diagnostic);
}
}
}

private static int? FindFirstUnusedIdentifierIndex(SemanticModel model, int docOffset, string baseName)
{
if (model.GetSpeculativeSymbolInfo(docOffset, SyntaxFactory.IdentifierName(baseName), SpeculativeBindingOption.BindAsExpression).Symbol is null)
{
return 0;
}

for (int i = 1; i < int.MaxValue; i++)
{
if (model.GetSpeculativeSymbolInfo(docOffset, SyntaxFactory.IdentifierName($"{baseName}{i}"), SpeculativeBindingOption.BindAsExpression).Symbol is null)
{
return i;
}
}
return 0;
}

private static async Task<Document> UseDisabledMarshallingEquivalentAsync(SyntaxNode node, Document document, CancellationToken ct)
{
var editor = await DocumentEditor.CreateAsync(document, ct).ConfigureAwait(false);
var identifierGenerator = new IdentifierGenerator(editor.SemanticModel, node.SpanStart);
var addUnsafeToEnclosingMethod = TryRewriteMethodCall(node, editor, identifierGenerator, addRenameAnnotation: true, ct);

if (addUnsafeToEnclosingMethod)
{
AddUnsafeModifierToEnclosingMethod(editor, node);
}

return editor.GetChangedDocument();
}

private static bool TryRewriteMethodCall(SyntaxNode node, DocumentEditor editor, IdentifierGenerator pointerIdentifierGenerator, bool addRenameAnnotation, CancellationToken ct)
{
var operation = (IInvocationOperation)editor.SemanticModel.GetOperation(node, ct);
InvocationExpressionSyntax syntax = (InvocationExpressionSyntax)operation.Syntax;

if (operation.TargetMethod.Name == "SizeOf")
{
if (operation.TargetMethod.IsGenericMethod)
{
if (operation.TargetMethod.TypeArguments[0].IsUnmanagedType)
{
editor.ReplaceNode(syntax, SyntaxFactory.SizeOfExpression((TypeSyntax)editor.Generator.TypeExpression(operation.TargetMethod.TypeArguments[0])));
return true;
}
}
else if (operation.Arguments[0].Value is ITypeOfOperation { TypeOperand.IsUnmanagedType: true } typeOf)
{
editor.ReplaceNode(syntax, SyntaxFactory.SizeOfExpression(GetTypeOfTypeSyntax((TypeOfExpressionSyntax)typeOf.Syntax)));
Copy link
Member

Choose a reason for hiding this comment

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

Consider handling trivia.

return true;
}
}
if (operation.TargetMethod.Name == "StructureToPtr" && operation.Arguments[0].Value.Type.IsUnmanagedType)
{
editor.ReplaceNode(syntax,
editor.Generator.AssignmentStatement(
SyntaxFactory.PrefixUnaryExpression(SyntaxKind.PointerIndirectionExpression,
(ExpressionSyntax)editor.Generator.CastExpression(editor.SemanticModel.Compilation.CreatePointerTypeSymbol(operation.Arguments[0].Value.Type),
operation.Arguments[1].Value.Syntax)),
operation.Arguments[0].Value.Syntax));
return true;
}
if (operation.TargetMethod.Name == "PtrToStructure")
{
ITypeSymbol type;
if (operation.TargetMethod.IsGenericMethod && operation.Arguments.Length == 1)
{
type = operation.TargetMethod.TypeArguments[0];
}
else if (operation.TargetMethod.ReturnType.SpecialType == SpecialType.System_Object
&& operation.Arguments.Length == 2
&& operation.Arguments[1].Value is ITypeOfOperation typeOf)
{
type = typeOf.TypeOperand;
}
else
{
return false;
}

if (operation.Arguments.Length > 0)
{
SyntaxNode replacementNode;
IOperation pointer = operation.Arguments[0].Value;
if (type.IsNullableValueType() && type.GetNullableValueTypeUnderlyingType() is ITypeSymbol { IsUnmanagedType: true } underlyingType)
{
var nonNullPtrIdentifier = pointerIdentifierGenerator.NextIdentifier();
if (nonNullPtrIdentifier is null)
{
// We couldn't generate an identifier to use, so don't update the call
return false;
}

SyntaxAnnotation renameIdentiferAnnotation = RenameAnnotation.Create();

IdentifierNameSyntax nonNullPtrIdentifierNode = SyntaxFactory.IdentifierName(nonNullPtrIdentifier);

if (addRenameAnnotation)
{
nonNullPtrIdentifierNode = nonNullPtrIdentifierNode.WithAdditionalAnnotations(renameIdentiferAnnotation);
}

var pointerCast = editor.Generator.CastExpression(
editor.SemanticModel.Compilation.CreatePointerTypeSymbol(underlyingType),
pointer.Syntax);

// Parse from a string since we're limited in SyntaxFactory methods due to the Roslyn version we build against.
// Use a dummy identifier for the expression since we want to replace it with `pointerCast` from above anyway
// to preserve the annotations that SyntaxGenerator provides.
var nullCheckAndDecl = (IsPatternExpressionSyntax)SyntaxFactory.ParseExpression($"x is not null and var {nonNullPtrIdentifier}");
nullCheckAndDecl = nullCheckAndDecl.WithExpression((ExpressionSyntax)pointerCast);
replacementNode = editor.Generator.ConditionalExpression(
nullCheckAndDecl,
SyntaxFactory.PrefixUnaryExpression(SyntaxKind.PointerIndirectionExpression, nonNullPtrIdentifierNode),
editor.Generator.CastExpression(operation.TargetMethod.ReturnType, editor.Generator.NullLiteralExpression()));
}
else if (type is { IsUnmanagedType: true })
{
replacementNode = editor.Generator.CastExpression(operation.TargetMethod.ReturnType,
SyntaxFactory.ParenthesizedExpression(SyntaxFactory.PrefixUnaryExpression(SyntaxKind.PointerIndirectionExpression,
(ExpressionSyntax)editor.Generator.CastExpression(
editor.SemanticModel.Compilation.CreatePointerTypeSymbol(type),
pointer.Syntax))));
}
else
{
return false;
}
editor.ReplaceNode(syntax, replacementNode);
return true;
}
}

return false;

static TypeSyntax GetTypeOfTypeSyntax(TypeOfExpressionSyntax syntax)
{
return syntax.Type;
}
}

private static void AddUnsafeModifierToEnclosingMethod(DocumentEditor editor, SyntaxNode syntax)
{
var enclosingMethod = FindEnclosingMethod(syntax);

static BaseMethodDeclarationSyntax? FindEnclosingMethod(SyntaxNode syntax)
{
while (syntax.Parent is not (null or BaseMethodDeclarationSyntax))
{
syntax = syntax.Parent;
}

return (BaseMethodDeclarationSyntax?)syntax.Parent;
}

editor.SetModifiers(enclosingMethod, editor.Generator.GetModifiers(enclosingMethod).WithIsUnsafe(true));
}
}
}
2 changes: 2 additions & 0 deletions src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
CA1420 | Interoperability | Warning | FeatureUnsupportedWhenRuntimeMarshallingDisabled, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1420)
CA1421 | Interoperability | Info | MethodUsesRuntimeMarshallingEvenWhenMarshallingDisabled, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421)
CA1849 | Performance | Disabled | UseAsyncMethodInAsyncContext, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1849)
CA1850 | Performance | Info | PreferHashDataOverComputeHashAnalyzer, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1850)
CA1851 | Performance | Disabled | AvoidMultipleEnumerations, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851)
Expand Down
Loading