Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if DEBUG
Comment thread
EgorBo marked this conversation as resolved.
using System.Collections.Immutable;
using System.Composition;
using System.Threading.Tasks;
using ILLink.CodeFixProvider;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace ILLink.CodeFix
{
/// <summary>
/// Fixes compiler diagnostic <c>CS9389</c> by marking an unclassified extern member <c>unsafe</c>.
/// The generated contract is intentionally conservative so developers can replace it with <c>safe</c> after audit.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AddUnsafeToExternCodeFixProvider)), Shared]
public sealed class AddUnsafeToExternCodeFixProvider : Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider
{
public const string ExternMemberRequiresUnsafeOrSafeDiagnosticId = "CS9389";

private static LocalizableString CodeFixTitle =>
new LocalizableResourceString(
nameof(Resources.AddUnsafeToExternCodeFixTitle),
Resources.ResourceManager,
typeof(Resources));

public override ImmutableArray<string> FixableDiagnosticIds =>
[ExternMemberRequiresUnsafeOrSafeDiagnosticId];

public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

public override Task RegisterCodeFixesAsync(CodeFixContext context) =>
UnsafeModifierCodeFixHelpers.RegisterAddUnsafeCodeFixAsync(
context,
CodeFixTitle,
static declaration => declaration is BaseMethodDeclarationSyntax
or PropertyDeclarationSyntax
or IndexerDeclarationSyntax
or EventDeclarationSyntax
or EventFieldDeclarationSyntax
or LocalFunctionStatementSyntax);
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if DEBUG
using System.Collections.Immutable;
using System.Composition;
using System.Threading.Tasks;
using ILLink.CodeFixProvider;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace ILLink.CodeFix
{
/// <summary>
/// Fixes compiler diagnostic <c>CS9392</c> by marking field-like members in explicit or extended layouts <c>unsafe</c>.
/// Primary-constructor parameters are intentionally unsupported because C# cannot place the modifier on them.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AddUnsafeToFieldCodeFixProvider)), Shared]
public sealed class AddUnsafeToFieldCodeFixProvider : Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider
{
public const string FieldRequiresUnsafeOrSafeDiagnosticId = "CS9392";

private static LocalizableString CodeFixTitle =>
new LocalizableResourceString(
nameof(Resources.AddUnsafeToFieldCodeFixTitle),
Resources.ResourceManager,
typeof(Resources));

public override ImmutableArray<string> FixableDiagnosticIds =>
[FieldRequiresUnsafeOrSafeDiagnosticId];

public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

public override Task RegisterCodeFixesAsync(CodeFixContext context) =>
UnsafeModifierCodeFixHelpers.RegisterAddUnsafeCodeFixAsync(
context,
CodeFixTitle,
static declaration => declaration is FieldDeclarationSyntax
or PropertyDeclarationSyntax
or EventFieldDeclarationSyntax);
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if DEBUG
using System.Collections.Immutable;
using System.Composition;
using System.Threading.Tasks;
using ILLink.CodeFixProvider;
using ILLink.Shared;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace ILLink.CodeFix
{
/// <summary>
/// Fixes analyzer diagnostic <c>IL5006</c> by adding <c>unsafe</c> to a pointer or function-pointer signature.
/// This preserves the caller-unsafe behavior that legacy memory-safety rules inferred from those signatures.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AddUnsafeToPointerSignatureCodeFixProvider)), Shared]
public sealed class AddUnsafeToPointerSignatureCodeFixProvider : Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider
{
private static LocalizableString CodeFixTitle =>
new LocalizableResourceString(
nameof(Resources.AddUnsafeToPointerSignatureCodeFixTitle),
Resources.ResourceManager,
typeof(Resources));

public override ImmutableArray<string> FixableDiagnosticIds =>
[DiagnosticId.PointerSignatureRequiresUnsafe.AsString()];

public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

public override Task RegisterCodeFixesAsync(CodeFixContext context) =>
UnsafeModifierCodeFixHelpers.RegisterAddUnsafeCodeFixAsync(
context,
CodeFixTitle,
static declaration => declaration is BaseMethodDeclarationSyntax
or PropertyDeclarationSyntax
or IndexerDeclarationSyntax
or EventDeclarationSyntax
or EventFieldDeclarationSyntax
or FieldDeclarationSyntax
or LocalFunctionStatementSyntax);
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
<PackageReference Condition="'$(DotNetBuildSourceOnly)' == 'true'" Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(MicrosoftCodeAnalysisVersion)" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\ILLink.RoslynAnalyzer\UnsafeMigrationSyntaxHelpers.cs" Link="UnsafeMigrationSyntaxHelpers.cs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ILLink.RoslynAnalyzer\ILLink.RoslynAnalyzer.csproj" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if DEBUG
using System.Collections.Immutable;
using System.Composition;
using System.Threading.Tasks;
using ILLink.CodeFixProvider;
using ILLink.RoslynAnalyzer;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace ILLink.CodeFix
{
/// <summary>
/// Fixes <c>CS9377</c> and unsafe-specific <c>CS0106</c> diagnostics by removing the invalid <c>unsafe</c> modifier.
/// The shared <c>CS0106</c> ID is filtered so modifiers unrelated to unsafe are left to their own fixes.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RemoveInvalidUnsafeCodeFixProvider)), Shared]
public sealed class RemoveInvalidUnsafeCodeFixProvider : Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider
{
public const string UnsafeModifierHasNoEffectDiagnosticId = "CS9377";
public const string InvalidModifierDiagnosticId = "CS0106";

private static LocalizableString CodeFixTitle =>
new LocalizableResourceString(
nameof(Resources.RemoveInvalidUnsafeCodeFixTitle),
Resources.ResourceManager,
typeof(Resources));

public override ImmutableArray<string> FixableDiagnosticIds =>
[UnsafeModifierHasNoEffectDiagnosticId, InvalidModifierDiagnosticId];

public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
Diagnostic diagnostic = context.Diagnostics[0];
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root is null)
return;

SyntaxNode targetNode = root.FindNode(
diagnostic.Location.SourceSpan,
getInnermostNodeForTie: true);
if (UnsafeModifierCodeFixHelpers.FindDeclaration(targetNode) is not { } declaration
|| !UnsafeMigrationSyntaxHelpers.HasModifier(declaration, SyntaxKind.UnsafeKeyword))
{
return;
}

// CS0106 is shared by every invalid modifier. Restrict this fixer to declaration shapes where
// Roslyn specifically reports unsafe as invalid, rather than inspecting localized message text.
if (diagnostic.Id == InvalidModifierDiagnosticId && !IsInvalidUnsafeDeclaration(declaration))
return;

string title = CodeFixTitle.ToString();
context.RegisterCodeFix(
CodeAction.Create(
title,
cancellationToken => UnsafeModifierCodeFixHelpers.RemoveUnsafeModifierAsync(
context.Document,
declaration,
cancellationToken),
title),
diagnostic);
}

/// <summary>
/// Identifies the well-formed declarations for which Roslyn reports unsafe-specific <c>CS0106</c>.
/// </summary>
private static bool IsInvalidUnsafeDeclaration(SyntaxNode declaration) =>
declaration switch
{
EnumDeclarationSyntax => true,
FieldDeclarationSyntax { Modifiers: var modifiers } => modifiers.Any(SyntaxKind.ConstKeyword),
_ => false,
};
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if DEBUG
using System.Collections.Immutable;
using System.Composition;
using System.Threading.Tasks;
using ILLink.CodeFixProvider;
using ILLink.RoslynAnalyzer;
using ILLink.Shared;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;

namespace ILLink.CodeFix
{
/// <summary>
/// Fixes <c>IL5005</c> by removing undocumented legacy unsafe scopes that became caller contracts under unsafe-v2.
/// Pointer signatures and field-like <c>CS9392</c> declarations retain <c>unsafe</c> for compatibility and safety.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RemoveUndocumentedUnsafeCodeFixProvider)), Shared]
public sealed class RemoveUndocumentedUnsafeCodeFixProvider : Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider
{
private static LocalizableString RemoveCodeFixTitle =>
new LocalizableResourceString(
nameof(Resources.RemoveUndocumentedUnsafeCodeFixTitle),
Resources.ResourceManager,
typeof(Resources));

public override ImmutableArray<string> FixableDiagnosticIds =>
[DiagnosticId.UnsafeMemberMissingSafetyDocumentation.AsString()];

public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
Diagnostic diagnostic = context.Diagnostics[0];
// Pointer signatures were already caller-unsafe under legacy rules. Field-like explicit and extended
// layout declarations must also keep a marker for CS9392, and default to unsafe until manually audited.
if (diagnostic.Properties.ContainsKey(UnsafeMemberMissingSafetyDocumentationAnalyzer.PointerSignatureProperty)
|| diagnostic.Properties.ContainsKey(UnsafeMemberMissingSafetyDocumentationAnalyzer.RequiresExplicitSafetyModifierProperty))
{
return;
}

var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root is null)
return;

SyntaxNode targetNode = root.FindNode(
diagnostic.Location.SourceSpan,
getInnermostNodeForTie: true);
if (UnsafeModifierCodeFixHelpers.FindDeclaration(targetNode) is not { } declaration
|| !UnsafeMigrationSyntaxHelpers.HasModifier(declaration, SyntaxKind.UnsafeKeyword))
{
return;
}

string title = RemoveCodeFixTitle.ToString();

context.RegisterCodeFix(
CodeAction.Create(
title,
cancellationToken => UnsafeModifierCodeFixHelpers.RemoveUnsafeModifierAsync(
context.Document,
declaration,
cancellationToken),
title),
diagnostic);
}
}
}
#endif
15 changes: 15 additions & 0 deletions src/tools/illink/src/ILLink.CodeFix/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,21 @@
<data name="RequiresUnsafeCodeFixTitle" xml:space="preserve">
<value>Add 'unsafe' to parent method</value>
</data>
<data name="AddUnsafeToExternCodeFixTitle" xml:space="preserve">
<value>Mark extern member 'unsafe'</value>
</data>
<data name="RemoveInvalidUnsafeCodeFixTitle" xml:space="preserve">
<value>Remove invalid 'unsafe' modifier</value>
</data>
<data name="RemoveUndocumentedUnsafeCodeFixTitle" xml:space="preserve">
<value>Remove undocumented 'unsafe' modifier</value>
</data>
<data name="AddUnsafeToPointerSignatureCodeFixTitle" xml:space="preserve">
<value>Add 'unsafe' to pointer signature</value>
</data>
<data name="AddUnsafeToFieldCodeFixTitle" xml:space="preserve">
<value>Mark field-like member 'unsafe'</value>
</data>
<data name="UconditionalSuppressMessageCodeFixTitle" xml:space="preserve">
<value>Add UnconditionalSuppressMessage attribute to parent method</value>
</data>
Expand Down
Loading
Loading