Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
47 changes: 41 additions & 6 deletions eng/liveILLink.targets
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,42 @@
<!-- NOTE: these targets should be a no-op, unless _RequiresLiveILLink, computed below, is true. -->

<PropertyGroup>
<_RequiresLiveILLinkExplicitlySet Condition="'$(_RequiresLiveILLink)' != ''">true</_RequiresLiveILLinkExplicitlySet>

<!-- 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 -->
<_RequiresLiveILLink Condition="'$(_RequiresLiveILLink)' == '' And (
<_RequiresLiveILLinkTasks Condition="'$(_RequiresLiveILLinkTasks)' == '' And '$(_RequiresLiveILLinkExplicitlySet)' == 'true' And '$(_RequiresLiveILLink)' == 'true'">true</_RequiresLiveILLinkTasks>
<_RequiresLiveILLinkTasks Condition="'$(_RequiresLiveILLinkTasks)' == '' And '$(_RequiresLiveILLinkExplicitlySet)' != 'true' 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>
'$(EnableSingleFileAnalyzer)' == 'true')">true</_RequiresLiveILLinkTasks>

<EnableUnsafeAnalyzer Condition="'$(EnableUnsafeMigration)' == 'true' And ('$(_RequiresLiveILLinkExplicitlySet)' != 'true' Or '$(_RequiresLiveILLink)' == 'true')">true</EnableUnsafeAnalyzer>
<_RequiresLiveUnsafeAnalyzer Condition="'$(EnableUnsafeAnalyzer)' == 'true'">true</_RequiresLiveUnsafeAnalyzer>
<_RequiresLiveILLink Condition="'$(_RequiresLiveILLink)' == '' And (
'$(_RequiresLiveILLinkTasks)' == 'true' Or
'$(_RequiresLiveUnsafeAnalyzer)' == 'true')">true</_RequiresLiveILLink>
<Features Condition="'$(EnableUnsafeMigration)' == 'true' And '$(_RequiresLiveILLink)' == 'true'">$(Features);updated-memory-safety-rules</Features>
</PropertyGroup>

<PropertyGroup Condition="'$(_RequiresLiveILLink)' == 'true'">
<_ILLinkTasksSourceDir>$(ToolsProjectRoot)illink\src\ILLink.Tasks\</_ILLinkTasksSourceDir>
<ILLinkAnalyzersPropsPath>$(ToolsProjectRoot)illink\src\ILLink.RoslynAnalyzer\build\Microsoft.NET.ILLink.Analyzers.props</ILLinkAnalyzersPropsPath>
</PropertyGroup>

<Import Project="$(_ILLinkTasksSourceDir)build\Microsoft.NET.ILLink.Tasks.props" Condition="'$(_RequiresLiveILLink)' == 'true'" />
<Import Project="$(_ILLinkTasksSourceDir)build\Microsoft.NET.ILLink.Tasks.props" Condition="'$(_RequiresLiveILLinkTasks)' == 'true'" />
<Import Project="$(ILLinkAnalyzersPropsPath)" Condition="'$(_RequiresLiveILLink)' == 'true' And '$(_RequiresLiveILLinkTasks)' != 'true'" />

<PropertyGroup Condition="'$(_RequiresLiveILLink)' == 'true'">
<PropertyGroup Condition="'$(_RequiresLiveILLinkTasks)' == 'true'">
<!-- Don't use SDK's trimming functionality. -->
<_RequiresILLinkPack>false</_RequiresILLinkPack>
<ILLinkTasksAssembly Condition="'$(MSBuildRuntimeType)' == 'Core'">$(ToolsILLinkDir)net\ILLink.Tasks.dll</ILLinkTasksAssembly>
<ILLinkTasksAssembly Condition="'$(MSBuildRuntimeType)' != 'Core'">$(ToolsILLinkDir)netframework\ILLink.Tasks.dll</ILLinkTasksAssembly>
</PropertyGroup>

<ItemGroup Condition="'$(_RequiresLiveILLink)' == 'true'">
<ItemGroup Condition="'$(_RequiresLiveILLinkTasks)' == 'true'">
<!-- The assembly shouldn't be referenced, nor promoted to a package dependency, nor copied to the output directory. -->
<ProjectReference Include="$(_ILLinkTasksSourceDir)ILLink.Tasks.csproj"
ReferenceOutputAssembly="false"
Expand All @@ -41,6 +51,9 @@
<SetTargetFramework Condition="'$(MSBuildRuntimeType)' != 'Core'">TargetFramework=$(NetFrameworkToolCurrent)</SetTargetFramework>
</ProjectReference>

</ItemGroup>

<ItemGroup Condition="'$(_RequiresLiveILLink)' == 'true'">
<!-- Need to reference the analyzer project separately, because there's no easy way to get it as a transitive reference of ILLink.Tasks.csproj -->
<ProjectReference Include="$(_ILLinkTasksSourceDir)..\ILLink.RoslynAnalyzer\ILLink.RoslynAnalyzer.csproj"
ReferenceOutputAssembly="false"
Expand All @@ -56,4 +69,26 @@
SetConfiguration="Configuration=$(ToolsConfiguration)" />
</ItemGroup>

<ItemGroup Condition="'$(EnableUnsafeMigration)' == 'true' And
'$(_RequiresLiveILLink)' == 'true' And
'$(DesignTimeBuild)' == 'true' And
'$(IsNETCoreAppSrc)' == 'true' And
'$(AssemblyName)' != 'System.Private.CoreLib' And
Exists('$(ArtifactsBinDir)ref\$(TargetFramework)\System.Runtime.dll') And
Exists('$(ArtifactsBinDir)System.Private.CoreLib\ref\$(Configuration)\$(TargetFramework)\System.Private.CoreLib.dll')">
<ProjectReference Remove="@(ProjectReference)" />
<Reference Include="$(ArtifactsBinDir)ref\$(TargetFramework)\*.dll"
Exclude="$(ArtifactsBinDir)ref\$(TargetFramework)\$(AssemblyName).dll"
Private="false" />
<Analyzer Include="$(ArtifactsBinDir)ILLink.RoslynAnalyzer\$(ToolsConfiguration)\netstandard2.0\ILLink.RoslynAnalyzer.dll"
Condition="Exists('$(ArtifactsBinDir)ILLink.RoslynAnalyzer\$(ToolsConfiguration)\netstandard2.0\ILLink.RoslynAnalyzer.dll')" />
<Analyzer Include="$(ArtifactsBinDir)ILLink.CodeFixProvider\$(ToolsConfiguration)\netstandard2.0\ILLink.CodeFixProvider.dll"
Condition="Exists('$(ArtifactsBinDir)ILLink.CodeFixProvider\$(ToolsConfiguration)\netstandard2.0\ILLink.CodeFixProvider.dll')" />
Comment thread
EgorBo marked this conversation as resolved.
Outdated
<Reference Include="System.Private.CoreLib"
Condition="Exists('$(ArtifactsBinDir)System.Private.CoreLib\ref\$(Configuration)\$(TargetFramework)\System.Private.CoreLib.dll')">
<HintPath>$(ArtifactsBinDir)System.Private.CoreLib\ref\$(Configuration)\$(TargetFramework)\System.Private.CoreLib.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>

</Project>
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
132 changes: 132 additions & 0 deletions src/tools/illink/src/ILLink.CodeFix/UnsafeCodeFixHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// 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.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__";

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;
}

var updates = UnsafeMigrationAnalysis.GetDeclarationUpdates(semanticModel, cancellationToken);
if (updates.IsEmpty)
return document;

var 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 (var 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);
}

private static SyntaxNode AddUnsafeModifier(
SyntaxNode declaration,
SyntaxGenerator generator)
=> declaration is AccessorDeclarationSyntax accessor
? accessor
.WithModifiers(accessor.Modifiers.Insert(
0,
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());
Comment thread
EgorBo marked this conversation as resolved.
Outdated

public static UnsafeStatementSyntax CreateUnsafeStatement(params StatementSyntax[] statements)
{
if (statements.Length > 0)
{
statements[0] = statements[0].WithLeadingTrivia(
CreateSafetyCommentTrivia().AddRange(statements[0].GetLeadingTrivia()));
}

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

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

if (template.ContainsDiagnostics)
return null;

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

if (placeholder is null)
return null;

return template.ReplaceNode(placeholder, expression.WithoutTrivia())
.WithTriviaFrom(expression);
Comment thread
EgorBo marked this conversation as resolved.
Outdated
}

public static bool ContainsDirectives(SyntaxNode node)
=> node.DescendantTrivia(descendIntoTrivia: true).Any(static trivia => trivia.IsDirective);

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}\r\n");
return declaration.WithLeadingTrivia(
leadingTrivia
.AddRange(documentation)
.AddRange(indentation));
}

private static SyntaxTriviaList CreateSafetyCommentTrivia()
=> SyntaxFactory.ParseLeadingTrivia($"{SafetyComment}\r\n");
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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.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;

namespace ILLink.CodeFix
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UnsafeModifierMigrationCodeFixProvider)), Shared]
public sealed class UnsafeModifierMigrationCodeFixProvider : Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider
{
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)
{
string title = CodeFixTitle.ToString();
context.RegisterCodeFix(
CodeAction.Create(
title,
cancellationToken => RemoveUnsafeModifiersAsync(context.Document, cancellationToken),
title),
context.Diagnostics);

return Task.CompletedTask;
}
Comment thread
EgorBo marked this conversation as resolved.
Outdated

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;
}

var removals = UnsafeMigrationAnalysis.GetModifierRemovals(semanticModel, cancellationToken);
if (removals.IsEmpty)
return document;

var 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