Skip to content
Draft
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
2 changes: 2 additions & 0 deletions eng/liveILLink.targets
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
<PropertyGroup>
<!-- Keep these conditions in sync with _RequiresILLinkPack in
https://github.com/dotnet/sdk/blob/main/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets -->
<EnableUnsafeAnalyzer Condition="'$(EnableUnsafeMigration)' == 'true' And '$(_RequiresLiveILLink)' != 'false'">true</EnableUnsafeAnalyzer>
<_RequiresLiveILLink Condition="'$(_RequiresLiveILLink)' == '' And (
'$(PublishAot)' == 'true' Or
'$(IsAotCompatible)' == 'true' Or '$(EnableAotAnalyzer)' == 'true' Or
'$(PublishTrimmed)' == 'true' Or
'$(IsTrimmable)' == 'true' Or '$(EnableTrimAnalyzer)' == 'true' Or
'$(EnableSingleFileAnalyzer)' == 'true' Or
'$(EnableUnsafeAnalyzer)' == 'true')">true</_RequiresLiveILLink>
<Features Condition="'$(EnableUnsafeMigration)' == 'true' And '$(_RequiresLiveILLink)' == 'true'">$(Features);updated-memory-safety-rules</Features>
</PropertyGroup>

<PropertyGroup Condition="'$(_RequiresLiveILLink)' == 'true'">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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 RoslynCodeFixProvider = Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider;

namespace ILLink.CodeFix;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AddUnsafeToExternCodeFixProvider)), Shared]
public sealed class AddUnsafeToExternCodeFixProvider : RoslynCodeFixProvider
{
private static LocalizableString CodeFixTitle => new LocalizableResourceString(
nameof(Resources.AddUnsafeModifierCodeFixTitle),
Resources.ResourceManager,
typeof(Resources));

public override ImmutableArray<string> FixableDiagnosticIds => ["CS9389"];

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

public override Task RegisterCodeFixesAsync(CodeFixContext context)
=> UnsafeModifierCodeFixHelpers.RegisterCodeFixAsync(
context,
CodeFixTitle.ToString(),
shouldHaveUnsafeModifier: true);
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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 RoslynCodeFixProvider = Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider;

namespace ILLink.CodeFix;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AddUnsafeToPointerSignatureCodeFixProvider)), Shared]
public sealed class AddUnsafeToPointerSignatureCodeFixProvider : RoslynCodeFixProvider
{
private static LocalizableString CodeFixTitle => new LocalizableResourceString(
nameof(Resources.AddUnsafeModifierCodeFixTitle),
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.RegisterCodeFixAsync(
context,
CodeFixTitle.ToString(),
shouldHaveUnsafeModifier: true);
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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.Globalization;
using System.Threading.Tasks;
using ILLink.CodeFixProvider;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using RoslynCodeFixProvider = Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider;

namespace ILLink.CodeFix;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RemoveInvalidUnsafeCodeFixProvider)), Shared]
public sealed class RemoveInvalidUnsafeCodeFixProvider : RoslynCodeFixProvider
{
private static LocalizableString CodeFixTitle => new LocalizableResourceString(
nameof(Resources.RemoveUnsafeModifierCodeFixTitle),
Resources.ResourceManager,
typeof(Resources));

public override ImmutableArray<string> FixableDiagnosticIds => ["CS0106", "CS9377"];

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

public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
if (context.Diagnostics[0] is { Id: "CS0106" } diagnostic &&
!diagnostic.GetMessage(CultureInfo.InvariantCulture)
.Contains("'unsafe'"))
{
return Task.CompletedTask;
}

return UnsafeModifierCodeFixHelpers.RegisterCodeFixAsync(
context,
CodeFixTitle.ToString(),
shouldHaveUnsafeModifier: false);
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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 RoslynCodeFixProvider = Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider;

namespace ILLink.CodeFix;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RemoveUndocumentedUnsafeCodeFixProvider)), Shared]
public sealed class RemoveUndocumentedUnsafeCodeFixProvider : RoslynCodeFixProvider
{
private static LocalizableString CodeFixTitle => new LocalizableResourceString(
nameof(Resources.RemoveUnsafeModifierCodeFixTitle),
Resources.ResourceManager,
typeof(Resources));

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

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

public override Task RegisterCodeFixesAsync(CodeFixContext context)
=> UnsafeModifierCodeFixHelpers.RegisterCodeFixAsync(
context,
CodeFixTitle.ToString(),
shouldHaveUnsafeModifier: false);
}
#endif
6 changes: 6 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,12 @@
<data name="RequiresUnsafeCodeFixTitle" xml:space="preserve">
<value>Add 'unsafe' to parent method</value>
</data>
<data name="AddUnsafeModifierCodeFixTitle" xml:space="preserve">
<value>Add 'unsafe' modifier</value>
</data>
<data name="RemoveUnsafeModifierCodeFixTitle" xml:space="preserve">
<value>Remove 'unsafe' modifier</value>
</data>
<data name="UconditionalSuppressMessageCodeFixTitle" xml:space="preserve">
<value>Add UnconditionalSuppressMessage attribute to parent method</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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;
using System.Threading;
using System.Threading.Tasks;
using ILLink.RoslynAnalyzer;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;

namespace ILLink.CodeFix;

internal static class UnsafeModifierCodeFixHelpers
{
public static Task RegisterCodeFixAsync(
CodeFixContext context,
string title,
bool shouldHaveUnsafeModifier)
{
if (!IsMigrationEnabled(context.Document))
return Task.CompletedTask;

Diagnostic diagnostic = context.Diagnostics[0];
context.RegisterCodeFix(
CodeAction.Create(
title,
cancellationToken => SetUnsafeModifierAsync(
context.Document,
diagnostic,
shouldHaveUnsafeModifier,
cancellationToken),
title),
diagnostic);

return Task.CompletedTask;
}

private static async Task<Document> SetUnsafeModifierAsync(
Document document,
Diagnostic diagnostic,
bool shouldHaveUnsafeModifier,
CancellationToken cancellationToken)
{
if (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false) is not { } root ||
UnsafeMemberAnalysis.FindDeclaration(
root,
diagnostic.Location.SourceSpan.Start) is not { } declaration)
{
return document;
}

SyntaxGenerator generator = SyntaxGenerator.GetGenerator(document);
SyntaxNode replacement = declaration is AccessorDeclarationSyntax accessor &&
shouldHaveUnsafeModifier &&
!UnsafeMemberAnalysis.HasUnsafeModifier(accessor)
? accessor
.WithModifiers(accessor.Modifiers.Add(
SyntaxFactory.Token(SyntaxKind.UnsafeKeyword)
.WithTrailingTrivia(SyntaxFactory.Space)))
.WithKeyword(accessor.Keyword.WithLeadingTrivia())
.WithLeadingTrivia(accessor.GetLeadingTrivia())
: generator.WithModifiers(
declaration,
generator.GetModifiers(declaration).WithIsUnsafe(shouldHaveUnsafeModifier))
.WithLeadingTrivia(declaration.GetLeadingTrivia());

return document.WithSyntaxRoot(root.ReplaceNode(declaration, replacement));
}

private static bool IsMigrationEnabled(Document document)
=> document.Project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue(
$"build_property.{MSBuildPropertyOptionNames.EnableUnsafeMigration}",
out string? value) &&
string.Equals(value?.Trim(), "true", StringComparison.OrdinalIgnoreCase);
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ public static class MSBuildPropertyOptionNames
public const string IncludeAllContentForSelfExtract = nameof(IncludeAllContentForSelfExtract);
public const string EnableTrimAnalyzer = nameof(EnableTrimAnalyzer);
public const string EnableAotAnalyzer = nameof(EnableAotAnalyzer);
#if DEBUG
public const string EnableUnsafeMigration = nameof(EnableUnsafeMigration);
#endif
public const string VerifyReferenceAotCompatibility = nameof(VerifyReferenceAotCompatibility);
public const string VerifyReferenceTrimCompatibility = nameof(VerifyReferenceTrimCompatibility);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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.Linq;
using ILLink.Shared;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace ILLink.RoslynAnalyzer;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class PointerSignatureRequiresUnsafeAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor s_rule =
DiagnosticDescriptors.GetDiagnosticDescriptor(
DiagnosticId.PointerSignatureRequiresUnsafe,
diagnosticSeverity: DiagnosticSeverity.Info);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [s_rule];

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(static context =>
{
if (!context.Options.IsMSBuildPropertyValueTrue(MSBuildPropertyOptionNames.EnableUnsafeMigration))
return;

context.RegisterSyntaxNodeAction(AnalyzeDeclaration, UnsafeMemberAnalysis.DeclarationKinds);
});
}

private static void AnalyzeDeclaration(SyntaxNodeAnalysisContext context)
{
if (!UnsafeMemberAnalysis.IsCandidate(context.Node) ||
context.Node is AccessorDeclarationSyntax ||
UnsafeMemberAnalysis.HasSafetyDocumentation(context.Node) ||
!UnsafeMemberAnalysis.HasPointerInSignature(
context.Node,
context.SemanticModel,
context.CancellationToken) ||
UnsafeMemberAnalysis.HasUnsafeModifier(context.Node))
{
return;
}

if (context.Node is BasePropertyDeclarationSyntax
{
AccessorList.Accessors: var accessors
} &&
accessors.Any(UnsafeMemberAnalysis.HasUnsafeModifier))
{
foreach (AccessorDeclarationSyntax accessor in accessors.Where(static accessor =>
UnsafeMemberAnalysis.IsPropertyAccessor(accessor) &&
!UnsafeMemberAnalysis.HasUnsafeModifier(accessor)))
{
ReportDiagnostic(context, accessor);
}

return;
}

ReportDiagnostic(context, context.Node);
}

private static void ReportDiagnostic(
SyntaxNodeAnalysisContext context,
SyntaxNode declaration)
=> context.ReportDiagnostic(Diagnostic.Create(
s_rule,
UnsafeMemberAnalysis.GetDeclarationLocation(declaration)));
}
#endif
Loading
Loading