Skip to content

Commit

Permalink
Port single-file analyzer (dotnet/linker#1665)
Browse files Browse the repository at this point in the history
* Add single-file analyzer

* Add tests

* Use str constant

* Update Resources

* Update tests

* PR feedback

* Remove mysterious filename from comments

* Add helper method for single-file diagnostic verification

* Refactor MSBuild properties

* Add test with PublishSingleFile not set

Commit migrated from dotnet/linker@a86658f
  • Loading branch information
mateoatr committed Dec 9, 2020
1 parent 78528e6 commit 8077175
Show file tree
Hide file tree
Showing 24 changed files with 650 additions and 28 deletions.
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/tools/illink/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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// 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
{
public static class MSBuildPropertyOptionNames
{
public const string PublishSingleFile = nameof (PublishSingleFile);
public const string IncludeAllContentForSelfExtract = nameof (IncludeAllContentForSelfExtract);
public const string PublishTrimmed = nameof (PublishTrimmed);
}
}
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/tools/illink/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/tools/illink/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/tools/illink/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/tools/illink/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/tools/illink/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/tools/illink/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

0 comments on commit 8077175

Please sign in to comment.