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

Add support for single file analysis #1691

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
76 changes: 76 additions & 0 deletions src/linker/Linker.Dataflow/ReflectionMethodBodyScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,11 @@ enum IntrinsicId
AppDomain_CreateInstanceFrom,
AppDomain_CreateInstanceFromAndUnwrap,
Assembly_CreateInstance,
Assembly_Location,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we also add AssemblyName_CodeBase, AssemblyName_EscapedCodeBase, Module_Name, and Module_FullyQualifiedName?

Assembly_CodeBase,
Assembly_EscapedCodeBase,
Assembly_GetFile,
Assembly_GetFiles,
RuntimeReflectionExtensions_GetRuntimeEvent,
RuntimeReflectionExtensions_GetRuntimeField,
RuntimeReflectionExtensions_GetRuntimeMethod,
Expand Down Expand Up @@ -481,6 +486,31 @@ static IntrinsicId GetIntrinsicIdForMethod (MethodDefinition calledMethod)
&& calledMethod.HasParameterOfType (0, "System", "String")
=> IntrinsicId.Assembly_CreateInstance,

// System.Reflection.Assembly.Location ()
"get_Location" when calledMethod.IsDeclaredOnType ("System.Reflection", "Assembly")
&& calledMethod.Parameters.Count == 0
=> IntrinsicId.Assembly_Location,

// System.Reflection.Assembly.CodeBase ()
"get_CodeBase" when calledMethod.IsDeclaredOnType ("System.Reflection", "AssemblyName")
&& calledMethod.Parameters.Count == 0
=> IntrinsicId.Assembly_CodeBase,

// System.Reflection.Assembly.EscapedCodeBase ()
"get_EscapedCodeBase" when calledMethod.IsDeclaredOnType ("System.Reflection", "AssemblyName")
&& calledMethod.Parameters.Count == 0
=> IntrinsicId.Assembly_EscapedCodeBase,

// System.Reflection.Assembly.GetFile (string)
"GetFile" when calledMethod.IsDeclaredOnType ("System.Reflection", "Assembly")
&& calledMethod.HasParameterOfType (0, "System", "String")
=> IntrinsicId.Assembly_GetFile,

// System.Reflection.Assembly.GetFiles ()
"GetFiles" when calledMethod.IsDeclaredOnType ("System.Reflection", "Assembly")
&& calledMethod.Parameters.Count == 0
=> IntrinsicId.Assembly_GetFiles,

// System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor (RuntimeTypeHandle type)
"RunClassConstructor" when calledMethod.IsDeclaredOnType ("System.Runtime.CompilerServices", "RuntimeHelpers")
&& calledMethod.HasParameterOfType (0, "System", "RuntimeTypeHandle")
Expand Down Expand Up @@ -1227,6 +1257,52 @@ methodParams[argsParam] is ArrayValue arrayValue &&
reflectionContext.RecordUnrecognizedPattern (2058, $"Parameters passed to method '{calledMethodDefinition.GetDisplayName ()}' cannot be analyzed. Consider using methods 'System.Type.GetType' and `System.Activator.CreateInstance` instead.");
break;

//
// System.Reflection.Assembly
//
// Location ()
//
case IntrinsicId.Assembly_Location when _context.SingleFileAnalysis == true:
_context.LogWarning ($"'{calledMethodDefinition.GetDisplayName ()}' 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'.", 3000, callingMethodDefinition);
break;

//
// System.Reflection.Assembly
//
// CodeBase ()
//
case IntrinsicId.Assembly_CodeBase when _context.SingleFileAnalysis == true:
_context.LogWarning ($"'{calledMethodDefinition.GetDisplayName ()}' 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'.", 3000, callingMethodDefinition);
Copy link
Contributor

@mateoatr mateoatr Dec 12, 2020

Choose a reason for hiding this comment

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

This actually throws, not return an empty string. I think we should add new warnings for this and other APIs (other than Assembly.Location and Assembly.GetFiles).

Copy link
Contributor

@mateoatr mateoatr Dec 12, 2020

Choose a reason for hiding this comment

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

And now that we have these warnings being produced by the linker, we should probably add them to the docs and close #1498.

break;

//
// System.Reflection.Assembly
//
// EscapedCodeBase ()
//
case IntrinsicId.Assembly_EscapedCodeBase when _context.SingleFileAnalysis == true:
_context.LogWarning ($"'{calledMethodDefinition.GetDisplayName ()}' 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'.", 3000, callingMethodDefinition);
break;

//
// System.Reflection.Assembly
//
// GetFile (string fileName)
//
case IntrinsicId.Assembly_GetFile when _context.SingleFileAnalysis == true:
_context.LogWarning ($"Assemblies embedded in a single-file app cannot have additional files in the manifest.", 3001, callingMethodDefinition);
break;

//
// System.Reflection.Assembly
//
// GetFiles ()
//
case IntrinsicId.Assembly_GetFiles when _context.SingleFileAnalysis == true:
_context.LogWarning ($"Assemblies embedded in a single-file app cannot have additional files in the manifest.", 3001, callingMethodDefinition);
break;


//
// System.Runtime.CompilerServices.RuntimeHelpers
//
Expand Down
6 changes: 6 additions & 0 deletions src/linker/Linker/Driver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,12 @@ protected int SetupContext (ILogger customLogger = null)
context.NoWarn.UnionWith (ProcessWarningCodes (noWarnArgument));
continue;

case "--single-file-analysis":
if (!GetBoolParam (token, l => context.SingleFileAnalysis = l))
return -1;

continue;

case "--warnaserror":
case "--warnaserror+":
var warningList = GetNextStringValue ();
Expand Down
6 changes: 6 additions & 0 deletions src/linker/Linker/LinkContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public class LinkContext : IDisposable
readonly Dictionary<string, string> _parameters;
bool _linkSymbols;
bool _keepTypeForwarderOnlyAssemblies;
bool _singleFileAnalysis;
bool _ignoreUnresolved;

readonly AssemblyResolver _resolver;
Expand Down Expand Up @@ -106,6 +107,11 @@ public bool KeepTypeForwarderOnlyAssemblies {
set { _keepTypeForwarderOnlyAssemblies = value; }
}

public bool SingleFileAnalysis {
Copy link
Contributor

@mateoatr mateoatr Dec 12, 2020

Choose a reason for hiding this comment

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

nit: similar methods in LinkContext tend to be named IsSomething, maybe name this IsSingleFile?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, why expand the getter/settter.

public bool SingleFileAnalysis { get; set; }

get { return _singleFileAnalysis; }
set { _singleFileAnalysis = value; }
}

#if FEATURE_ILLINK
public readonly bool KeepMembersForDebugger = true;
#else
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// 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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Mono.Linker.Tests.Cases.Expectations.Assertions;
using Mono.Linker.Tests.Cases.Expectations.Metadata;

namespace Mono.Linker.Tests.Cases.SingleFileAnalysis
{
public class SingleFileAnalysisIsNotSet
{
public static void Main ()
{
AssemblyLocationDoesNotWarn ();
AssemblyGetFileDoesNotWarn ();
}

[Kept]
[LogDoesNotContain ("IL3000")]
static string AssemblyLocationDoesNotWarn () => Assembly.GetExecutingAssembly ().Location;

[Kept]
[LogDoesNotContain ("IL3001")]
static void AssemblyGetFileDoesNotWarn ()
{
var a = Assembly.GetExecutingAssembly ();
_ = a.GetFile ("/some/file/path");
_ = a.GetFiles ();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// 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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Mono.Linker.Tests.Cases.Expectations.Assertions;
using Mono.Linker.Tests.Cases.Expectations.Metadata;

namespace Mono.Linker.Tests.Cases.SingleFileAnalysis
{
[SetupLinkerArgument ("--single-file-analysis", "true")]
public class WarnOnSingleFileDangerousPatterns
Copy link
Contributor

Choose a reason for hiding this comment

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

If we move this tests here, can we remove them from ILLink.RoslynAnalyzer.Tests and call them in the analyzer with RunTest? Similar to what we already have for RequiresCapability tests.

{
public static void Main ()
{
GetExecutingAssemblyLocation ();
AssemblyProperties ();
AssemblyMethods ();
AssemblyNameAttributes ();
FalsePositive ();
}

[Kept]
[ExpectedWarning ("IL3000",
"'System.Reflection.Assembly.Location.get' 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'")]
static string GetExecutingAssemblyLocation () => Assembly.GetExecutingAssembly ().Location;

[Kept]
[ExpectedWarning ("IL3000",
"'System.Reflection.Assembly.Location.get' 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'")]
static void AssemblyProperties ()
{
var a = Assembly.GetExecutingAssembly ();
_ = a.Location;
// below methods are marked as obsolete in 5.0
// _ = a.CodeBase;
// _ = a.EscapedCodeBase;
}

[Kept]
[ExpectedWarning ("IL3001",
"Assemblies embedded in a single-file app cannot have additional files in the manifest.")]
[ExpectedWarning ("IL3001",
"Assemblies embedded in a single-file app cannot have additional files in the manifest.")]
static void AssemblyMethods ()
{
var a = Assembly.GetExecutingAssembly ();
_ = a.GetFile ("/some/file/path");
_ = a.GetFiles ();
}

[Kept]
[ExpectedWarning ("IL3000",
"'System.Reflection.AssemblyName.CodeBase.get' 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'.")]
[ExpectedWarning ("IL3000",
"'System.Reflection.AssemblyName.EscapedCodeBase.get' 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'.")]

static void AssemblyNameAttributes ()
{
var a = Assembly.GetExecutingAssembly ().GetName ();
_ = a.CodeBase;
_ = a.EscapedCodeBase;
}

// This is an OK use of Location and GetFile since these assemblies were loaded from
// a file, but the linker is conservative
[Kept]
[ExpectedWarning ("IL3000",
"'System.Reflection.Assembly.Location.get' 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'")]
[ExpectedWarning ("IL3001",
"Assemblies embedded in a single-file app cannot have additional files in the manifest.")]
static void FalsePositive ()
{
var a = Assembly.LoadFrom ("/some/path/not/in/bundle");
_ = a.Location;
_ = a.GetFiles ();
}
}
}
5 changes: 5 additions & 0 deletions test/Mono.Linker.Tests/TestCases/TestDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,11 @@ public static IEnumerable<TestCaseData> LinkAttributesTests ()
return NUnitCasesBySuiteName ("LinkAttributes");
}

public static IEnumerable<TestCaseData> SingleFileAnalysisTests ()
{
return NUnitCasesBySuiteName ("SingleFileAnalysis");
}

public static TestCaseCollector CreateCollector ()
{
GetDirectoryPaths (out string rootSourceDirectory, out string testCaseAssemblyPath);
Expand Down
6 changes: 6 additions & 0 deletions test/Mono.Linker.Tests/TestCases/TestSuites.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,12 @@ public void LinkAttributesTests (TestCase testCase)
Run (testCase);
}

[TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.SingleFileAnalysisTests))]
public void SingleFileAnalysisTests (TestCase testCase)
{
Run (testCase);
}

protected virtual void Run (TestCase testCase)
{
var runner = new TestRunner (new ObjectFactory ());
Expand Down