Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 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
49 changes: 43 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,28 @@
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') And
Exists('$(ArtifactsBinDir)ILLink.RoslynAnalyzer\$(ToolsConfiguration)\netstandard2.0\ILLink.RoslynAnalyzer.dll') And
Exists('$(ArtifactsBinDir)ILLink.CodeFixProvider\$(ToolsConfiguration)\netstandard2.0\ILLink.CodeFixProvider.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')" />
<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
207 changes: 207 additions & 0 deletions src/tools/illink/src/ILLink.CodeFix/UnsafeCodeFixHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// 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.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)
=> document.Project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue(
$"build_property.{MSBuildPropertyOptionNames.EnableUnsafeMigration}",
out string? value) &&
string.Equals(value?.Trim(), "true", System.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;
}

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

// A multi-variable event field whose events implement a mix of safe and unsafe contracts
// cannot take a single 'unsafe' modifier (it would make the safe events illegally unsafe).
// Split it so 'unsafe' is applied only to the variables that require it.
if (update.AddUnsafeModifier &&
update.Declaration is EventFieldDeclarationSyntax originalEventField &&
originalEventField.Declaration.Variables.Count > 1)
{
ImmutableArray<string> unsafeVariables = UnsafeMigrationAnalysis.GetUnsafeContractEventVariableNames(
originalEventField, semanticModel, cancellationToken);
if (unsafeVariables.Length > 0 &&
unsafeVariables.Length < originalEventField.Declaration.Variables.Count)
{
changedRoot = SplitEventFieldDeclaration(
changedRoot,
(EventFieldDeclarationSyntax)declaration,
[.. unsafeVariables],
generator);
continue;
}
}

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 SplitEventFieldDeclaration(
SyntaxNode root,
EventFieldDeclarationSyntax declaration,
HashSet<string> unsafeVariables,
SyntaxGenerator generator)
{
SyntaxTriviaList leadingTrivia = declaration.GetLeadingTrivia();
SyntaxTriviaList indentation = SyntaxFactory.TriviaList(
leadingTrivia
.Reverse()
.TakeWhile(static trivia => trivia.IsKind(SyntaxKind.WhitespaceTrivia))
.Reverse());
SyntaxTriviaList trailingTrivia = declaration.GetTrailingTrivia();

SeparatedSyntaxList<VariableDeclaratorSyntax> variables = declaration.Declaration.Variables;
var replacements = new List<EventFieldDeclarationSyntax>(variables.Count);
for (int i = 0; i < variables.Count; i++)
{
EventFieldDeclarationSyntax single = declaration
.WithDeclaration(declaration.Declaration.WithVariables(
SyntaxFactory.SingletonSeparatedList(variables[i].WithoutTrivia())))
.WithoutTrivia();
Comment thread
EgorBo marked this conversation as resolved.
Outdated

if (unsafeVariables.Contains(variables[i].Identifier.ValueText))
single = (EventFieldDeclarationSyntax)AddUnsafeModifier(single, generator);

replacements.Add(single
.WithLeadingTrivia(i == 0 ? leadingTrivia : indentation)
.WithTrailingTrivia(i == variables.Count - 1
? trailingTrivia
: SyntaxFactory.TriviaList(SyntaxFactory.ElasticCarriageReturnLineFeed))
.WithAdditionalAnnotations(Formatter.Annotation));
}

return root.ReplaceNode(declaration, replacements);
}

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})",
options: s_unsafeParseOptions);

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)
.Add(SyntaxFactory.ElasticCarriageReturnLineFeed);
return declaration.WithLeadingTrivia(
leadingTrivia
.AddRange(documentation)
.AddRange(indentation));
}

private static SyntaxTriviaList CreateSafetyCommentTrivia()
=> SyntaxFactory.TriviaList(
SyntaxFactory.Comment(SafetyComment),
SyntaxFactory.ElasticCarriageReturnLineFeed);
}
}
#endif
Loading