Skip to content
Closed
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
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="UnsafeModifierMigrationCodeFixTitle" xml:space="preserve">
<value>Remove legacy unsafe modifiers</value>
</data>
<data name="UnsafeUsageMigrationCodeFixTitle" xml:space="preserve">
<value>Migrate unsafe usages</value>
</data>
<data name="UconditionalSuppressMessageCodeFixTitle" xml:space="preserve">
<value>Add UnconditionalSuppressMessage attribute to parent method</value>
</data>
Expand Down
155 changes: 155 additions & 0 deletions src/tools/illink/src/ILLink.CodeFix/UnsafeCodeFixHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// 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.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ILLink.RoslynAnalyzer;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;

namespace ILLink.CodeFix;

internal static class UnsafeCodeFixHelpers
{
private const string SafetyComment = "// SAFETY: Audit";
private const string SafetyDocumentation = "/// <safety>TODO: Audit.</safety>";
private const string UnsafeExpressionPlaceholder = "__unsafe_operand__";

private static readonly CSharpParseOptions s_unsafeParseOptions =
new CSharpParseOptions(LanguageVersion.Preview)
.WithFeatures([new("updated-memory-safety-rules", "")]);

public static bool IsMigrationEnabled(Document document)
=> IsMSBuildPropertyValueTrue(
document,
MSBuildPropertyOptionNames.EnableUnsafeMigration);

public static bool IsMSBuildPropertyValueTrue(
Document document,
string propertyName)
=> document.Project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue(
$"build_property.{propertyName}",
out string? value) &&
string.Equals(value?.Trim(), "true", StringComparison.OrdinalIgnoreCase);

public static async Task<Document> ApplyDeclarationUpdatesAsync(
Document document,
CancellationToken cancellationToken)
{
if (await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false) is not { } semanticModel ||
await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false) is not { } root)
{
return document;
}

ImmutableArray<UnsafeMigrationAnalysis.DeclarationUpdate> updates =
UnsafeMigrationAnalysis.GetDeclarationUpdates(semanticModel, cancellationToken);
if (updates.IsEmpty)
return document;

Dictionary<SyntaxNode, SyntaxAnnotation> annotations = updates.ToDictionary(
static update => update.Declaration,
static _ => new SyntaxAnnotation());

SyntaxNode changedRoot = root.ReplaceNodes(
annotations.Keys,
(original, rewritten) => rewritten.WithAdditionalAnnotations(annotations[original]));

SyntaxGenerator generator = SyntaxGenerator.GetGenerator(document);
foreach (UnsafeMigrationAnalysis.DeclarationUpdate update in updates)
{
SyntaxNode declaration = changedRoot.GetAnnotatedNodes(annotations[update.Declaration]).Single();
SyntaxNode replacement = declaration;

if (update.AddUnsafeModifier && !UnsafeMigrationAnalysis.HasUnsafeModifier(declaration))
replacement = AddUnsafeModifier(replacement, generator);

if (update.AddSafetyDocumentation && !UnsafeMigrationAnalysis.HasSafetyDocumentation(replacement))
replacement = AddSafetyDocumentation(replacement);

changedRoot = changedRoot.ReplaceNode(declaration, replacement);
}

return document.WithSyntaxRoot(changedRoot);
}

public static UnsafeStatementSyntax CreateUnsafeStatement(params StatementSyntax[] statements)
{
if (statements is [var first, ..])
{
statements[0] = first.WithLeadingTrivia(
CreateSafetyCommentTrivia().AddRange(first.GetLeadingTrivia()));
}

return SyntaxFactory.UnsafeStatement(SyntaxFactory.Block(statements))
.WithAdditionalAnnotations(Formatter.Annotation);
}

public static ExpressionSyntax? CreateUnsafeExpression(ExpressionSyntax expression)
{
ExpressionSyntax template = SyntaxFactory.ParseExpression(
$"unsafe(/* SAFETY: Audit */{UnsafeExpressionPlaceholder})",
options: s_unsafeParseOptions);

if (template.ContainsDiagnostics)
return null;

IdentifierNameSyntax? placeholder = template.DescendantNodesAndSelf()
.OfType<IdentifierNameSyntax>()
.SingleOrDefault(static identifier => identifier.Identifier.ValueText == UnsafeExpressionPlaceholder);

return placeholder is null
? null
: template.ReplaceNode(placeholder, expression.WithoutTrivia())
.WithTriviaFrom(expression);
}

private static SyntaxNode AddUnsafeModifier(
SyntaxNode declaration,
SyntaxGenerator generator)
=> declaration switch
{
AccessorDeclarationSyntax accessor => accessor
.WithModifiers(accessor.Modifiers.Add(
SyntaxFactory.Token(SyntaxKind.UnsafeKeyword)
.WithTrailingTrivia(SyntaxFactory.Space)))
.WithKeyword(accessor.Keyword.WithLeadingTrivia(SyntaxFactory.TriviaList()))
.WithLeadingTrivia(accessor.GetLeadingTrivia()),

_ => generator.WithModifiers(
declaration,
generator.GetModifiers(declaration).WithIsUnsafe(true))
.WithLeadingTrivia(declaration.GetLeadingTrivia())
};

private static SyntaxNode AddSafetyDocumentation(SyntaxNode declaration)
{
SyntaxTriviaList leadingTrivia = declaration.GetLeadingTrivia();
SyntaxTriviaList indentation = SyntaxFactory.TriviaList(
leadingTrivia
.Reverse()
.TakeWhile(static trivia => trivia.IsKind(SyntaxKind.WhitespaceTrivia))
.Reverse());
SyntaxTriviaList documentation = SyntaxFactory.ParseLeadingTrivia(SafetyDocumentation)
.Add(SyntaxFactory.ElasticCarriageReturnLineFeed);

return declaration.WithLeadingTrivia(
leadingTrivia
.AddRange(documentation)
.AddRange(indentation));
}

private static SyntaxTriviaList CreateSafetyCommentTrivia()
=> SyntaxFactory.TriviaList(
SyntaxFactory.Comment(SafetyComment),
SyntaxFactory.ElasticCarriageReturnLineFeed);
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
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.Editing;
using RoslynCodeFixProvider = Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider;

namespace ILLink.CodeFix;

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

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

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

public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
if (!UnsafeCodeFixHelpers.IsMigrationEnabled(context.Document))
return Task.CompletedTask;

string title = CodeFixTitle.ToString();
context.RegisterCodeFix(
CodeAction.Create(
title,
cancellationToken => RemoveUnsafeModifiersAsync(context.Document, cancellationToken),
title),
context.Diagnostics);

return Task.CompletedTask;
}

private static async Task<Document> RemoveUnsafeModifiersAsync(
Document document,
CancellationToken cancellationToken)
{
if (await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false) is not { } semanticModel ||
await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false) is not { } root)
{
return document;
}

ImmutableArray<UnsafeMigrationAnalysis.ModifierRemoval> removals =
UnsafeMigrationAnalysis.GetModifierRemovals(semanticModel, cancellationToken);
if (removals.IsEmpty)
return document;

Dictionary<SyntaxNode, SyntaxAnnotation> annotations = removals.ToDictionary(
static removal => removal.Declaration,
static _ => new SyntaxAnnotation());

SyntaxNode changedRoot = root.ReplaceNodes(
annotations.Keys,
(original, rewritten) => rewritten.WithAdditionalAnnotations(annotations[original]));

SyntaxGenerator generator = SyntaxGenerator.GetGenerator(document);
foreach (SyntaxAnnotation annotation in annotations.Values)
{
SyntaxNode declaration = changedRoot.GetAnnotatedNodes(annotation).Single();
SyntaxNode replacement = generator.WithModifiers(
declaration,
generator.GetModifiers(declaration).WithIsUnsafe(false))
.WithLeadingTrivia(declaration.GetLeadingTrivia());

changedRoot = changedRoot.ReplaceNode(declaration, replacement);
}

return document.WithSyntaxRoot(changedRoot);
}
}
#endif
Loading
Loading