Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port single-file analyzer #1665

Merged
merged 10 commits into from
Dec 9, 2020
Merged
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
31 changes: 31 additions & 0 deletions src/ILLink.RoslynAnalyzer/AnalyzerOptionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace ILLink.RoslynAnalyzer
{
internal static class AnalyzerOptionsExtensions
{
public static string? GetMSBuildPropertyValue (
this AnalyzerOptions options,
string optionName,
Compilation compilation)
{
// MSBuild property values should be set at compilation level, and cannot have different values per-tree.
// So, we default to first syntax tree.
var tree = compilation.SyntaxTrees.FirstOrDefault ();
if (tree is null) {
return null;
}

return options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue (
$"build_property.{optionName}", out var value)
? value
: null;
}
}
}
12 changes: 12 additions & 0 deletions src/ILLink.RoslynAnalyzer/DiagnosticCategory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace ILLink.RoslynAnalyzer
{
internal static class DiagnosticCategory
{
public const string SingleFile = nameof (SingleFile);
public const string Trimming = nameof (Trimming);
}
}
26 changes: 26 additions & 0 deletions src/ILLink.RoslynAnalyzer/MSBuildPropertyOptionNames.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Diagnostics;
using System.Linq;

namespace ILLink.RoslynAnalyzer
{
public static class MSBuildPropertyOptionNames
{
public const string PublishSingleFile = nameof (PublishSingleFile);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably need to add PublishTrimmed now since otherwise all the trim warnings will automatically be turned on during single-file

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't be better to think about the more general area than to hardcode the single narrow scenario? Are we going to add a property for each of remaining scenario which would benefit from the same logic?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps there's some broader feature work we could do, but we certainly need to recognize whether trimming or single-file are enabled, so we at least need to recognize the two MSBuild properties that already exist.

public const string IncludeAllContentForSelfExtract = nameof (IncludeAllContentForSelfExtract);
public const string PublishTrimmed = nameof (PublishTrimmed);
}

internal static class MSBuildPropertyOptionNamesHelpers
{
public static void VerifySupportedPropertyOptionName (string propertyOptionName)
mateoatr marked this conversation as resolved.
Show resolved Hide resolved
{
#if DEBUG
Debug.Assert (typeof (MSBuildPropertyOptionNames).GetFields ().Single (f => f.Name == propertyOptionName) != null);
#endif
}
}
}
27 changes: 13 additions & 14 deletions src/ILLink.RoslynAnalyzer/RequiresUnreferencedCodeAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

using System;
using System.Collections.Immutable;
using System.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
Expand All @@ -16,21 +15,13 @@ public class RequiresUnreferencedCodeAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "IL2026";

private static readonly LocalizableString s_title = new LocalizableResourceString (
nameof (RequiresUnreferencedCodeAnalyzer) + "Title",
Resources.ResourceManager,
typeof (Resources));
private static readonly LocalizableString s_messageFormat = new LocalizableResourceString (
nameof (RequiresUnreferencedCodeAnalyzer) + "Message",
Resources.ResourceManager,
typeof (Resources));
private const string s_category = "Trimming";

private static readonly DiagnosticDescriptor s_rule = new DiagnosticDescriptor (
DiagnosticId,
s_title,
s_messageFormat,
s_category,
new LocalizableResourceString (nameof (Resources.RequiresUnreferencedCodeAnalyzerTitle),
Resources.ResourceManager, typeof (Resources)),
new LocalizableResourceString (nameof (Resources.RequiresUnreferencedCodeAnalyzerMessage),
Resources.ResourceManager, typeof (Resources)),
DiagnosticCategory.Trimming,
DiagnosticSeverity.Warning,
isEnabledByDefault: true);

Expand All @@ -44,8 +35,16 @@ public override void Initialize (AnalysisContext context)
context.RegisterCompilationStartAction (context => {
var compilation = context.Compilation;

var isPublishTrimmed = context.Options.GetMSBuildPropertyValue (MSBuildPropertyOptionNames.PublishTrimmed, compilation);
if (!string.Equals (isPublishTrimmed?.Trim (), "true", StringComparison.OrdinalIgnoreCase)) {
return;
}

context.RegisterOperationAction (operationContext => {
var call = (IInvocationOperation) operationContext.Operation;
if (call.IsVirtual && call.TargetMethod.OverriddenMethod != null)
return;

CheckMethodOrCtorCall (operationContext, call.TargetMethod, call.Syntax.GetLocation ());
}, OperationKind.Invocation);

Expand Down
12 changes: 12 additions & 0 deletions src/ILLink.RoslynAnalyzer/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,16 @@
<data name="RequiresUnreferencedCodeAnalyzerMessage" xml:space="preserve">
<value>Calling '{0}' which has `RequiresUnreferencedCodeAttribute` can break functionality when trimming application code. {1}. {2}</value>
</data>
<data name="AvoidAssemblyLocationInSingleFileTitle" xml:space="preserve">
<value>Avoid accessing Assembly file path when publishing as a single file</value>
</data>
<data name="AvoidAssemblyLocationInSingleFileMessage" xml:space="preserve">
<value>'{0}' always returns an empty string for assemblies embedded in a single-file app. If the path to the app directory is needed, consider calling 'System.AppContext.BaseDirectory'.</value>
</data>
<data name="AvoidAssemblyGetFilesInSingleFileTitle" xml:space="preserve">
<value>Avoid accessing Assembly file path when publishing as a single file</value>
</data>
<data name="AvoidAssemblyGetFilesInSingleFileMessage" xml:space="preserve">
<value>'{0}' will throw for assemblies embedded in a single-file app</value>
</data>
</root>
141 changes: 141 additions & 0 deletions src/ILLink.RoslynAnalyzer/SingleFileAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace ILLink.RoslynAnalyzer
{
/// <summary>
/// IL3000, IL3001: Do not use Assembly file path in single-file publish
/// </summary>
[DiagnosticAnalyzer (LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class AvoidAssemblyLocationInSingleFile : DiagnosticAnalyzer
{
public const string IL3000 = nameof (IL3000);
public const string IL3001 = nameof (IL3001);

private static readonly DiagnosticDescriptor LocationRule = new DiagnosticDescriptor (
IL3000,
new LocalizableResourceString (nameof (Resources.AvoidAssemblyLocationInSingleFileTitle),
Resources.ResourceManager, typeof (Resources)),
new LocalizableResourceString (nameof (Resources.AvoidAssemblyLocationInSingleFileMessage),
Resources.ResourceManager, typeof (Resources)),
DiagnosticCategory.SingleFile,
DiagnosticSeverity.Warning,
isEnabledByDefault: true);

private static readonly DiagnosticDescriptor GetFilesRule = new DiagnosticDescriptor (
IL3001,
new LocalizableResourceString (nameof (Resources.AvoidAssemblyGetFilesInSingleFileTitle),
Resources.ResourceManager, typeof (Resources)),
new LocalizableResourceString (nameof (Resources.AvoidAssemblyGetFilesInSingleFileMessage),
Resources.ResourceManager, typeof (Resources)),
DiagnosticCategory.SingleFile,
DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create (LocationRule, GetFilesRule);

public override void Initialize (AnalysisContext context)
{
context.EnableConcurrentExecution ();
context.ConfigureGeneratedCodeAnalysis (GeneratedCodeAnalysisFlags.ReportDiagnostics);

context.RegisterCompilationStartAction (context => {
var compilation = context.Compilation;

var isSingleFilePublish = context.Options.GetMSBuildPropertyValue (MSBuildPropertyOptionNames.PublishSingleFile, compilation);
if (!string.Equals (isSingleFilePublish?.Trim (), "true", StringComparison.OrdinalIgnoreCase)) {
return;
}
var includesAllContent = context.Options.GetMSBuildPropertyValue (MSBuildPropertyOptionNames.IncludeAllContentForSelfExtract, compilation);
if (string.Equals (includesAllContent?.Trim (), "true", StringComparison.OrdinalIgnoreCase)) {
return;
}

var propertiesBuilder = ImmutableArray.CreateBuilder<IPropertySymbol> ();
var methodsBuilder = ImmutableArray.CreateBuilder<IMethodSymbol> ();

var assemblyType = compilation.GetTypeByMetadataName ("System.Reflection.Assembly");
if (assemblyType != null) {
// properties
AddIfNotNull (propertiesBuilder, TryGetSingleSymbol<IPropertySymbol> (assemblyType.GetMembers ("Location")));

// methods
methodsBuilder.AddRange (assemblyType.GetMembers ("GetFile").OfType<IMethodSymbol> ());
methodsBuilder.AddRange (assemblyType.GetMembers ("GetFiles").OfType<IMethodSymbol> ());
}

var assemblyNameType = compilation.GetTypeByMetadataName ("System.Reflection.AssemblyName");
if (assemblyNameType != null) {
AddIfNotNull (propertiesBuilder, TryGetSingleSymbol<IPropertySymbol> (assemblyNameType.GetMembers ("CodeBase")));
AddIfNotNull (propertiesBuilder, TryGetSingleSymbol<IPropertySymbol> (assemblyNameType.GetMembers ("EscapedCodeBase")));
}

var properties = propertiesBuilder.ToImmutable ();
var methods = methodsBuilder.ToImmutable ();

context.RegisterOperationAction (operationContext => {
var access = (IPropertyReferenceOperation) operationContext.Operation;
var property = access.Property;
if (!Contains (properties, property, SymbolEqualityComparer.Default)) {
return;
}

operationContext.ReportDiagnostic (Diagnostic.Create (LocationRule, access.Syntax.GetLocation (), property));
}, OperationKind.PropertyReference);

context.RegisterOperationAction (operationContext => {
var invocation = (IInvocationOperation) operationContext.Operation;
var targetMethod = invocation.TargetMethod;
if (!Contains (methods, targetMethod, SymbolEqualityComparer.Default)) {
return;
}

operationContext.ReportDiagnostic (Diagnostic.Create (GetFilesRule, invocation.Syntax.GetLocation (), targetMethod));
}, OperationKind.Invocation);

return;

static bool Contains<T, TComp> (ImmutableArray<T> list, T elem, TComp comparer)
where TComp : IEqualityComparer<T>
{
foreach (var e in list) {
if (comparer.Equals (e, elem)) {
return true;
}
}
return false;
}

static TSymbol? TryGetSingleSymbol<TSymbol> (ImmutableArray<ISymbol> members) where TSymbol : class, ISymbol
{
TSymbol? candidate = null;
foreach (var m in members) {
if (m is TSymbol tsym) {
if (candidate is null) {
candidate = tsym;
} else {
return null;
}
}
}
return candidate;
}

static void AddIfNotNull<TSymbol> (ImmutableArray<TSymbol>.Builder properties, TSymbol? p) where TSymbol : class, ISymbol
{
if (p != null) {
properties.Add (p);
}
}
});
}
}
}
20 changes: 20 additions & 0 deletions src/ILLink.RoslynAnalyzer/xlf/Resources.cs.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
<file datatype="xml" source-language="en" target-language="cs" original="../Resources.resx">
<body>
<trans-unit id="AvoidAssemblyGetFilesInSingleFileMessage">
<source>'{0}' will throw for assemblies embedded in a single-file app</source>
<target state="new">'{0}' will throw for assemblies embedded in a single-file app</target>
<note />
</trans-unit>
<trans-unit id="AvoidAssemblyGetFilesInSingleFileTitle">
<source>Avoid accessing Assembly file path when publishing as a single file</source>
<target state="new">Avoid accessing Assembly file path when publishing as a single file</target>
<note />
</trans-unit>
<trans-unit id="AvoidAssemblyLocationInSingleFileMessage">
<source>'{0}' always returns an empty string for assemblies embedded in a single-file app. If the path to the app directory is needed, consider calling 'System.AppContext.BaseDirectory'.</source>
<target state="new">'{0}' always returns an empty string for assemblies embedded in a single-file app. If the path to the app directory is needed, consider calling 'System.AppContext.BaseDirectory'.</target>
<note />
</trans-unit>
<trans-unit id="AvoidAssemblyLocationInSingleFileTitle">
<source>Avoid accessing Assembly file path when publishing as a single file</source>
<target state="new">Avoid accessing Assembly file path when publishing as a single file</target>
<note />
</trans-unit>
<trans-unit id="RequiresUnreferencedCodeAnalyzerMessage">
<source>Calling '{0}' which has `RequiresUnreferencedCodeAttribute` can break functionality when trimming application code. {1}. {2}</source>
<target state="new">Calling '{0}' which has `RequiresUnreferencedCodeAttribute` can break functionality when trimming application code. {1}. {2}</target>
Expand Down
20 changes: 20 additions & 0 deletions src/ILLink.RoslynAnalyzer/xlf/Resources.de.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
<file datatype="xml" source-language="en" target-language="de" original="../Resources.resx">
<body>
<trans-unit id="AvoidAssemblyGetFilesInSingleFileMessage">
<source>'{0}' will throw for assemblies embedded in a single-file app</source>
<target state="new">'{0}' will throw for assemblies embedded in a single-file app</target>
<note />
</trans-unit>
<trans-unit id="AvoidAssemblyGetFilesInSingleFileTitle">
<source>Avoid accessing Assembly file path when publishing as a single file</source>
<target state="new">Avoid accessing Assembly file path when publishing as a single file</target>
<note />
</trans-unit>
<trans-unit id="AvoidAssemblyLocationInSingleFileMessage">
<source>'{0}' always returns an empty string for assemblies embedded in a single-file app. If the path to the app directory is needed, consider calling 'System.AppContext.BaseDirectory'.</source>
<target state="new">'{0}' always returns an empty string for assemblies embedded in a single-file app. If the path to the app directory is needed, consider calling 'System.AppContext.BaseDirectory'.</target>
<note />
</trans-unit>
<trans-unit id="AvoidAssemblyLocationInSingleFileTitle">
<source>Avoid accessing Assembly file path when publishing as a single file</source>
<target state="new">Avoid accessing Assembly file path when publishing as a single file</target>
<note />
</trans-unit>
<trans-unit id="RequiresUnreferencedCodeAnalyzerMessage">
<source>Calling '{0}' which has `RequiresUnreferencedCodeAttribute` can break functionality when trimming application code. {1}. {2}</source>
<target state="new">Calling '{0}' which has `RequiresUnreferencedCodeAttribute` can break functionality when trimming application code. {1}. {2}</target>
Expand Down
20 changes: 20 additions & 0 deletions src/ILLink.RoslynAnalyzer/xlf/Resources.es.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
<file datatype="xml" source-language="en" target-language="es" original="../Resources.resx">
<body>
<trans-unit id="AvoidAssemblyGetFilesInSingleFileMessage">
<source>'{0}' will throw for assemblies embedded in a single-file app</source>
<target state="new">'{0}' will throw for assemblies embedded in a single-file app</target>
<note />
</trans-unit>
<trans-unit id="AvoidAssemblyGetFilesInSingleFileTitle">
<source>Avoid accessing Assembly file path when publishing as a single file</source>
<target state="new">Avoid accessing Assembly file path when publishing as a single file</target>
<note />
</trans-unit>
<trans-unit id="AvoidAssemblyLocationInSingleFileMessage">
<source>'{0}' always returns an empty string for assemblies embedded in a single-file app. If the path to the app directory is needed, consider calling 'System.AppContext.BaseDirectory'.</source>
<target state="new">'{0}' always returns an empty string for assemblies embedded in a single-file app. If the path to the app directory is needed, consider calling 'System.AppContext.BaseDirectory'.</target>
<note />
</trans-unit>
<trans-unit id="AvoidAssemblyLocationInSingleFileTitle">
<source>Avoid accessing Assembly file path when publishing as a single file</source>
<target state="new">Avoid accessing Assembly file path when publishing as a single file</target>
<note />
</trans-unit>
<trans-unit id="RequiresUnreferencedCodeAnalyzerMessage">
<source>Calling '{0}' which has `RequiresUnreferencedCodeAttribute` can break functionality when trimming application code. {1}. {2}</source>
<target state="new">Calling '{0}' which has `RequiresUnreferencedCodeAttribute` can break functionality when trimming application code. {1}. {2}</target>
Expand Down
20 changes: 20 additions & 0 deletions src/ILLink.RoslynAnalyzer/xlf/Resources.fr.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
<file datatype="xml" source-language="en" target-language="fr" original="../Resources.resx">
<body>
<trans-unit id="AvoidAssemblyGetFilesInSingleFileMessage">
<source>'{0}' will throw for assemblies embedded in a single-file app</source>
<target state="new">'{0}' will throw for assemblies embedded in a single-file app</target>
<note />
</trans-unit>
<trans-unit id="AvoidAssemblyGetFilesInSingleFileTitle">
<source>Avoid accessing Assembly file path when publishing as a single file</source>
<target state="new">Avoid accessing Assembly file path when publishing as a single file</target>
<note />
</trans-unit>
<trans-unit id="AvoidAssemblyLocationInSingleFileMessage">
<source>'{0}' always returns an empty string for assemblies embedded in a single-file app. If the path to the app directory is needed, consider calling 'System.AppContext.BaseDirectory'.</source>
<target state="new">'{0}' always returns an empty string for assemblies embedded in a single-file app. If the path to the app directory is needed, consider calling 'System.AppContext.BaseDirectory'.</target>
<note />
</trans-unit>
<trans-unit id="AvoidAssemblyLocationInSingleFileTitle">
<source>Avoid accessing Assembly file path when publishing as a single file</source>
<target state="new">Avoid accessing Assembly file path when publishing as a single file</target>
<note />
</trans-unit>
<trans-unit id="RequiresUnreferencedCodeAnalyzerMessage">
<source>Calling '{0}' which has `RequiresUnreferencedCodeAttribute` can break functionality when trimming application code. {1}. {2}</source>
<target state="new">Calling '{0}' which has `RequiresUnreferencedCodeAttribute` can break functionality when trimming application code. {1}. {2}</target>
Expand Down
Loading