PowerShell/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs
xtqqczze 883ca98dd7
Seal private classes (#15725)
* Seal private classes

* Fix CS0509

* Fix CS0628
2021-07-19 14:09:12 +05:00

1353 lines
52 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Internal;
using System.Reflection;
using System.Runtime.Loader;
using System.Security;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Text;
using PathType = System.IO.Path;
namespace Microsoft.PowerShell.Commands
{
/// <summary>
/// Languages supported for code generation.
/// </summary>
public enum Language
{
/// <summary>
/// The C# programming language.
/// </summary>
CSharp
}
/// <summary>
/// Types supported for the OutputAssembly parameter.
/// </summary>
public enum OutputAssemblyType
{
/// <summary>
/// A Dynamically linked library (DLL).
/// </summary>
Library,
/// <summary>
/// An executable application that targets the console subsystem.
/// </summary>
ConsoleApplication,
/// <summary>
/// An executable application that targets the graphical subsystem.
/// </summary>
WindowsApplication
}
/// <summary>
/// Adds a new type to the Application Domain.
/// This version is based on CodeAnalysis (Roslyn).
/// </summary>
[Cmdlet(VerbsCommon.Add, "Type", DefaultParameterSetName = FromSourceParameterSetName, HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2096601")]
[OutputType(typeof(Type))]
public sealed class AddTypeCommand : PSCmdlet
{
#region Parameters
/// <summary>
/// The source code of this generated type.
/// </summary>
[Parameter(Mandatory = true, Position = 0, ParameterSetName = FromSourceParameterSetName)]
[ValidateTrustedData]
public string TypeDefinition
{
get
{
return _sourceCode;
}
set
{
_sourceCode = value;
}
}
/// <summary>
/// The name of the type (class) used for auto-generated types.
/// </summary>
[Parameter(Mandatory = true, Position = 0, ParameterSetName = FromMemberParameterSetName)]
[ValidateTrustedData]
public string Name { get; set; }
/// <summary>
/// The source code of this generated method / member.
/// </summary>
[Parameter(Mandatory = true, Position = 1, ParameterSetName = FromMemberParameterSetName)]
public string[] MemberDefinition
{
get
{
return new string[] { _sourceCode };
}
set
{
_sourceCode = string.Empty;
if (value != null)
{
_sourceCode = string.Join("\n", value);
}
}
}
private string _sourceCode;
/// <summary>
/// The namespace used for the auto-generated type.
/// </summary>
[Parameter(ParameterSetName = FromMemberParameterSetName)]
[AllowNull]
[Alias("NS")]
public string Namespace { get; set; } = "Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes";
/// <summary>
/// Any using statements required by the auto-generated type.
/// </summary>
[Parameter(ParameterSetName = FromMemberParameterSetName)]
[ValidateNotNull()]
[Alias("Using")]
public string[] UsingNamespace { get; set; } = Array.Empty<string>();
/// <summary>
/// The path to the source code or DLL to load.
/// </summary>
[Parameter(Mandatory = true, Position = 0, ParameterSetName = FromPathParameterSetName)]
[ValidateTrustedData]
public string[] Path
{
get
{
return _paths;
}
set
{
if (value == null)
{
_paths = null;
return;
}
string[] pathValue = value;
List<string> resolvedPaths = new();
// Verify that the paths are resolved and valid
foreach (string path in pathValue)
{
// Try to resolve the path
Collection<string> newPaths = SessionState.Path.GetResolvedProviderPathFromPSPath(path, out ProviderInfo _);
// If it didn't resolve, add the original back
// for a better error message.
if (newPaths.Count == 0)
{
resolvedPaths.Add(path);
}
else
{
resolvedPaths.AddRange(newPaths);
}
}
ProcessPaths(resolvedPaths);
}
}
/// <summary>
/// The literal path to the source code or DLL to load.
/// </summary>
[Parameter(Mandatory = true, ParameterSetName = FromLiteralPathParameterSetName)]
[Alias("PSPath", "LP")]
[ValidateTrustedData]
public string[] LiteralPath
{
get
{
return _paths;
}
set
{
if (value == null)
{
_paths = null;
return;
}
List<string> resolvedPaths = new();
foreach (string path in value)
{
string literalPath = SessionState.Path.GetUnresolvedProviderPathFromPSPath(path);
resolvedPaths.Add(literalPath);
}
ProcessPaths(resolvedPaths);
}
}
private void ProcessPaths(List<string> resolvedPaths)
{
// Validate file extensions.
// Make sure we don't mix source files from different languages (if we support any other languages in future).
string activeExtension = null;
foreach (string path in resolvedPaths)
{
string currentExtension = PathType.GetExtension(path).ToUpperInvariant();
switch (currentExtension)
{
case ".CS":
Language = Language.CSharp;
break;
case ".DLL":
_loadAssembly = true;
break;
// Throw an error if it is an unrecognized extension
default:
ErrorRecord errorRecord = new(
new Exception(
StringUtil.Format(AddTypeStrings.FileExtensionNotSupported, currentExtension)),
"EXTENSION_NOT_SUPPORTED",
ErrorCategory.InvalidArgument,
currentExtension);
ThrowTerminatingError(errorRecord);
break;
}
if (activeExtension == null)
{
activeExtension = currentExtension;
}
else if (!string.Equals(activeExtension, currentExtension, StringComparison.OrdinalIgnoreCase))
{
// All files must have the same extension otherwise throw.
ErrorRecord errorRecord = new(
new Exception(
StringUtil.Format(AddTypeStrings.MultipleExtensionsNotSupported)),
"MULTIPLE_EXTENSION_NOT_SUPPORTED",
ErrorCategory.InvalidArgument,
currentExtension);
ThrowTerminatingError(errorRecord);
}
}
_paths = resolvedPaths.ToArray();
}
private string[] _paths;
/// <summary>
/// The name of the assembly to load.
/// </summary>
[Parameter(Mandatory = true, ParameterSetName = FromAssemblyNameParameterSetName)]
[Alias("AN")]
[ValidateTrustedData]
public string[] AssemblyName { get; set; }
private bool _loadAssembly = false;
/// <summary>
/// The language used to compile the source code.
/// Default is C#.
/// </summary>
[Parameter(ParameterSetName = FromSourceParameterSetName)]
[Parameter(ParameterSetName = FromMemberParameterSetName)]
public Language Language { get; set; } = Language.CSharp;
/// <summary>
/// Any reference DLLs to use in the compilation.
/// </summary>
[Parameter(ParameterSetName = FromSourceParameterSetName)]
[Parameter(ParameterSetName = FromMemberParameterSetName)]
[Parameter(ParameterSetName = FromPathParameterSetName)]
[Parameter(ParameterSetName = FromLiteralPathParameterSetName)]
[Alias("RA")]
public string[] ReferencedAssemblies
{
get
{
return _referencedAssemblies;
}
set
{
if (value != null) { _referencedAssemblies = value; }
}
}
private string[] _referencedAssemblies = Array.Empty<string>();
/// <summary>
/// The path to the output assembly.
/// </summary>
[Parameter(ParameterSetName = FromSourceParameterSetName)]
[Parameter(ParameterSetName = FromMemberParameterSetName)]
[Parameter(ParameterSetName = FromPathParameterSetName)]
[Parameter(ParameterSetName = FromLiteralPathParameterSetName)]
[Alias("OA")]
public string OutputAssembly
{
get
{
return _outputAssembly;
}
set
{
_outputAssembly = value;
if (_outputAssembly != null)
{
_outputAssembly = _outputAssembly.Trim();
// Try to resolve the path
ProviderInfo provider = null;
Collection<string> newPaths = new();
try
{
newPaths = SessionState.Path.GetResolvedProviderPathFromPSPath(_outputAssembly, out provider);
}
// Ignore the ItemNotFound -- we handle it.
catch (ItemNotFoundException) { }
ErrorRecord errorRecord = new(
new Exception(
StringUtil.Format(AddTypeStrings.OutputAssemblyDidNotResolve, _outputAssembly)),
"INVALID_OUTPUT_ASSEMBLY",
ErrorCategory.InvalidArgument,
_outputAssembly);
// If it resolved to a non-standard provider,
// generate an error.
if (!string.Equals("FileSystem", provider.Name, StringComparison.OrdinalIgnoreCase))
{
ThrowTerminatingError(errorRecord);
return;
}
// If it resolved to more than one path,
// generate an error.
if (newPaths.Count > 1)
{
ThrowTerminatingError(errorRecord);
return;
}
// It didn't resolve to any files. They may
// want to create the file.
else if (newPaths.Count == 0)
{
// We can't create one with wildcard characters
if (WildcardPattern.ContainsWildcardCharacters(_outputAssembly))
{
ThrowTerminatingError(errorRecord);
}
// Create the file
else
{
_outputAssembly = SessionState.Path.GetUnresolvedProviderPathFromPSPath(_outputAssembly);
}
}
// It resolved to a single file
else
{
_outputAssembly = newPaths[0];
}
}
}
}
private string _outputAssembly = null;
/// <summary>
/// The output type of the assembly.
/// </summary>
[Parameter(ParameterSetName = FromSourceParameterSetName)]
[Parameter(ParameterSetName = FromMemberParameterSetName)]
[Parameter(ParameterSetName = FromPathParameterSetName)]
[Parameter(ParameterSetName = FromLiteralPathParameterSetName)]
[Alias("OT")]
public OutputAssemblyType OutputType { get; set; } = OutputAssemblyType.Library;
/// <summary>
/// Flag to pass the resulting types along.
/// </summary>
[Parameter()]
public SwitchParameter PassThru { get; set; }
/// <summary>
/// Flag to ignore warnings during compilation.
/// </summary>
[Parameter(ParameterSetName = FromSourceParameterSetName)]
[Parameter(ParameterSetName = FromMemberParameterSetName)]
[Parameter(ParameterSetName = FromPathParameterSetName)]
[Parameter(ParameterSetName = FromLiteralPathParameterSetName)]
public SwitchParameter IgnoreWarnings { get; set; }
/// <summary>
/// Roslyn command line parameters.
/// https://github.com/dotnet/roslyn/blob/master/docs/compilers/CSharp/CommandLine.md
///
/// Parser options:
/// langversion:string - language version from:
/// [enum]::GetNames([Microsoft.CodeAnalysis.CSharp.LanguageVersion])
/// define:symbol list - preprocessor symbols:
/// /define:UNIX,DEBUG - CSharp
///
/// Compilation options:
/// optimize{+|-} - optimization level
/// parallel{+|-} - concurrent build
/// warnaserror{+|-} - report warnings to errors
/// warnaserror{+|-}:strings - report specific warnings to errors
/// warn:number - warning level (0-4) for CSharp
/// nowarn - disable all warnings
/// nowarn:strings - disable a list of individual warnings
/// usings:strings - ';'-delimited usings for CSharp
///
/// Emit options:
/// platform:string - limit which platforms this code can run on; must be x86, x64, Itanium, arm, AnyCPU32BitPreferred or anycpu (default)
/// delaysign{+|-} - delay-sign the assembly using only the public portion of the strong name key
/// keyfile:file - specifies a strong name key file
/// keycontainer:string - specifies a strong name key container
/// highentropyva{+|-} - enable high-entropy ASLR.
/// </summary>
[Parameter(ParameterSetName = FromSourceParameterSetName)]
[Parameter(ParameterSetName = FromMemberParameterSetName)]
[Parameter(ParameterSetName = FromPathParameterSetName)]
[Parameter(ParameterSetName = FromLiteralPathParameterSetName)]
[ValidateNotNullOrEmpty]
public string[] CompilerOptions { get; set; }
#endregion Parameters
#region GererateSource
private string GenerateTypeSource(string typeNamespace, string typeName, string sourceCodeText, Language language)
{
string usingSource = string.Format(
CultureInfo.InvariantCulture,
GetUsingTemplate(language), GetUsingSet(language));
string typeSource = string.Format(
CultureInfo.InvariantCulture,
GetMethodTemplate(language), typeName, sourceCodeText);
if (!string.IsNullOrEmpty(typeNamespace))
{
return usingSource + string.Format(
CultureInfo.InvariantCulture,
GetNamespaceTemplate(language), typeNamespace, typeSource);
}
else
{
return usingSource + typeSource;
}
}
// Get the -FromMember template for a given language
private static string GetMethodTemplate(Language language)
{
switch (language)
{
case Language.CSharp:
return
" public class {0}\n" +
" {{\n" +
" {1}\n" +
" }}\n";
}
throw PSTraceSource.NewNotSupportedException();
}
// Get the -FromMember namespace template for a given language
private static string GetNamespaceTemplate(Language language)
{
switch (language)
{
case Language.CSharp:
return
"namespace {0}\n" +
"{{\n" +
"{1}\n" +
"}}\n";
}
throw PSTraceSource.NewNotSupportedException();
}
// Get the -FromMember namespace template for a given language
private static string GetUsingTemplate(Language language)
{
switch (language)
{
case Language.CSharp:
return
"using System;\n" +
"using System.Runtime.InteropServices;\n" +
"{0}" +
"\n";
}
throw PSTraceSource.NewNotSupportedException();
}
// Generate the code for the using statements
private string GetUsingSet(Language language)
{
StringBuilder usingNamespaceSet = new();
switch (language)
{
case Language.CSharp:
foreach (string namespaceValue in UsingNamespace)
{
usingNamespaceSet.Append("using " + namespaceValue + ";\n");
}
break;
default:
throw PSTraceSource.NewNotSupportedException();
}
return usingNamespaceSet.ToString();
}
#endregion GererateSource
/// <summary>
/// Prevent code compilation in ConstrainedLanguage mode.
/// </summary>
protected override void BeginProcessing()
{
// Prevent code compilation in ConstrainedLanguage mode
if (SessionState.LanguageMode == PSLanguageMode.ConstrainedLanguage)
{
ThrowTerminatingError(
new ErrorRecord(
new PSNotSupportedException(AddTypeStrings.CannotDefineNewType),
nameof(AddTypeStrings.CannotDefineNewType),
ErrorCategory.PermissionDenied,
targetObject: null));
}
// 'ConsoleApplication' and 'WindowsApplication' types are currently not working in .NET Core
if (OutputType != OutputAssemblyType.Library)
{
ThrowTerminatingError(
new ErrorRecord(
new PSNotSupportedException(AddTypeStrings.AssemblyTypeNotSupported),
nameof(AddTypeStrings.AssemblyTypeNotSupported),
ErrorCategory.NotImplemented,
targetObject: OutputType));
}
}
/// <summary>
/// Generate and load the type(s).
/// </summary>
protected override void EndProcessing()
{
// Generate an error if they've specified an output
// assembly type without an output assembly
if (string.IsNullOrEmpty(_outputAssembly) && this.MyInvocation.BoundParameters.ContainsKey(nameof(OutputType)))
{
ErrorRecord errorRecord = new(
new Exception(
string.Format(
CultureInfo.CurrentCulture,
AddTypeStrings.OutputTypeRequiresOutputAssembly)),
"OUTPUTTYPE_REQUIRES_ASSEMBLY",
ErrorCategory.InvalidArgument,
OutputType);
ThrowTerminatingError(errorRecord);
return;
}
if (_loadAssembly)
{
// File extension is ".DLL" (ParameterSetName = FromPathParameterSetName or FromLiteralPathParameterSetName).
LoadAssemblies(_paths);
}
else if (ParameterSetName == FromAssemblyNameParameterSetName)
{
LoadAssemblies(AssemblyName);
}
else
{
// Process a source code from files or strings.
SourceCodeProcessing();
}
}
#region LoadAssembly
// We now ship .NET Core's reference assemblies with PowerShell, so that Add-Type can work
// in a predictable way and won't be broken when we move to newer version of .NET Core.
// The reference assemblies are located at '$PSHOME\ref' for pwsh.
//
// For applications that host PowerShell, the 'ref' folder will be deployed to the 'publish'
// folder, not where 'System.Management.Automation.dll' is located. So here we should use
// the entry assembly's location to construct the path to the 'ref' folder.
// For pwsh, the entry assembly is 'pwsh.dll', so the entry assembly's location is still
// $PSHOME.
// However, 'Assembly.GetEntryAssembly()' returns null when the managed code is called from
// unmanaged code (PowerShell WSMan remoting scenario), so in that case, we continue to use
// the location of 'System.Management.Automation.dll'.
private static readonly string s_netcoreAppRefFolder = PathType.Combine(
PathType.GetDirectoryName(
(Assembly.GetEntryAssembly() ?? typeof(PSObject).Assembly).Location),
"ref");
// Path to the folder where .NET Core runtime assemblies are located.
private static readonly string s_frameworkFolder = PathType.GetDirectoryName(typeof(object).Assembly.Location);
// These assemblies are always automatically added to ReferencedAssemblies.
private static readonly Lazy<PortableExecutableReference[]> s_autoReferencedAssemblies = new(InitAutoIncludedRefAssemblies);
// A HashSet of assembly names to be ignored if they are specified in '-ReferencedAssemblies'
private static readonly Lazy<HashSet<string>> s_refAssemblyNamesToIgnore = new(InitRefAssemblyNamesToIgnore);
// These assemblies are used, when ReferencedAssemblies parameter is not specified.
private static readonly Lazy<IEnumerable<PortableExecutableReference>> s_defaultAssemblies = new(InitDefaultRefAssemblies);
private bool InMemory { get { return string.IsNullOrEmpty(_outputAssembly); } }
// These dictionaries prevent reloading already loaded and unchanged code.
// We don't worry about unbounded growing of the cache because in .Net Core 2.0 we can not unload assemblies.
// TODO: review if we will be able to unload assemblies after migrating to .Net Core 2.1.
private static readonly HashSet<string> s_sourceTypesCache = new();
private static readonly Dictionary<int, Assembly> s_sourceAssemblyCache = new();
private static readonly string s_defaultSdkDirectory = Utils.DefaultPowerShellAppBase;
private const ReportDiagnostic defaultDiagnosticOption = ReportDiagnostic.Error;
private static readonly string[] s_writeInformationTags = new string[] { "PSHOST" };
private int _syntaxTreesHash;
private const string FromMemberParameterSetName = "FromMember";
private const string FromSourceParameterSetName = "FromSource";
private const string FromPathParameterSetName = "FromPath";
private const string FromLiteralPathParameterSetName = "FromLiteralPath";
private const string FromAssemblyNameParameterSetName = "FromAssemblyName";
private void LoadAssemblies(IEnumerable<string> assemblies)
{
foreach (string assemblyName in assemblies)
{
// CoreCLR doesn't allow re-load TPA assemblies with different API (i.e. we load them by name and now want to load by path).
// LoadAssemblyHelper helps us avoid re-loading them, if they already loaded.
Assembly assembly = LoadAssemblyHelper(assemblyName) ?? Assembly.LoadFrom(ResolveAssemblyName(assemblyName, false));
if (PassThru)
{
WriteTypes(assembly);
}
}
}
/// <summary>
/// Initialize the list of reference assemblies that will be used when '-ReferencedAssemblies' is not specified.
/// </summary>
private static IEnumerable<PortableExecutableReference> InitDefaultRefAssemblies()
{
// Define number of reference assemblies distributed with PowerShell.
const int maxPowershellRefAssemblies = 160;
const int capacity = maxPowershellRefAssemblies + 1;
var defaultRefAssemblies = new List<PortableExecutableReference>(capacity);
foreach (string file in Directory.EnumerateFiles(s_netcoreAppRefFolder, "*.dll", SearchOption.TopDirectoryOnly))
{
defaultRefAssemblies.Add(MetadataReference.CreateFromFile(file));
}
// Add System.Management.Automation.dll
defaultRefAssemblies.Add(MetadataReference.CreateFromFile(typeof(PSObject).Assembly.Location));
// We want to avoid reallocating the internal array, so we assert if the list capacity has increased.
Diagnostics.Assert(
defaultRefAssemblies.Capacity <= capacity,
$"defaultRefAssemblies was resized because of insufficient initial capacity! A capacity of {defaultRefAssemblies.Count} is required.");
return defaultRefAssemblies;
}
/// <summary>
/// Initialize the set of assembly names that should be ignored when they are specified in '-ReferencedAssemblies'.
/// - System.Private.CoreLib.ni.dll - the runtime dll that contains most core/primitive types
/// - System.Private.Uri.dll - the runtime dll that contains 'System.Uri' and related types
/// Referencing these runtime dlls may cause ambiguous type identity or other issues.
/// - System.Runtime.dll - the corresponding reference dll will be automatically included
/// - System.Runtime.InteropServices.dll - the corresponding reference dll will be automatically included.
/// </summary>
private static HashSet<string> InitRefAssemblyNamesToIgnore()
{
return new HashSet<string>(StringComparer.OrdinalIgnoreCase) {
PathType.GetFileName(typeof(object).Assembly.Location),
PathType.GetFileName(typeof(Uri).Assembly.Location),
PathType.GetFileName(GetReferenceAssemblyPathBasedOnType(typeof(object))),
PathType.GetFileName(GetReferenceAssemblyPathBasedOnType(typeof(SecureString)))
};
}
/// <summary>
/// Initialize the list of reference assemblies that will be automatically added when '-ReferencedAssemblies' is specified.
/// </summary>
private static PortableExecutableReference[] InitAutoIncludedRefAssemblies()
{
return new PortableExecutableReference[] {
MetadataReference.CreateFromFile(GetReferenceAssemblyPathBasedOnType(typeof(object))),
MetadataReference.CreateFromFile(GetReferenceAssemblyPathBasedOnType(typeof(SecureString)))
};
}
/// <summary>
/// Get the path of reference assembly where the type is declared.
/// </summary>
private static string GetReferenceAssemblyPathBasedOnType(Type type)
{
string refAsmFileName = PathType.GetFileName(ClrFacade.GetAssemblies(type.FullName).First().Location);
return PathType.Combine(s_netcoreAppRefFolder, refAsmFileName);
}
private string ResolveAssemblyName(string assembly, bool isForReferenceAssembly)
{
ErrorRecord errorRecord;
// if it's a path, resolve it
if (assembly.Contains(PathType.DirectorySeparatorChar) || assembly.Contains(PathType.AltDirectorySeparatorChar))
{
if (PathType.IsPathRooted(assembly))
{
return assembly;
}
else
{
var paths = SessionState.Path.GetResolvedPSPathFromPSPath(assembly);
if (paths.Count > 0)
{
return paths[0].Path;
}
else
{
errorRecord = new ErrorRecord(
new Exception(
string.Format(ParserStrings.ErrorLoadingAssembly, assembly)),
"ErrorLoadingAssembly",
ErrorCategory.InvalidOperation,
assembly);
ThrowTerminatingError(errorRecord);
return null;
}
}
}
string refAssemblyDll = assembly;
if (!assembly.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
{
// It could be a short assembly name or a full assembly name, but we
// always want the short name to find the corresponding assembly file.
var assemblyName = new AssemblyName(assembly);
refAssemblyDll = assemblyName.Name + ".dll";
}
// We look up in reference/framework only when it's for resolving reference assemblies.
// In case of 'Add-Type -AssemblyName' scenario, we don't attempt to resolve against framework assemblies because
// 1. Explicitly loading a framework assembly usually is not necessary in PowerShell 6+.
// 2. A user should use assembly name instead of path if they want to explicitly load a framework assembly.
if (isForReferenceAssembly)
{
// If it's for resolving a reference assembly, then we look in NetCoreApp ref assemblies first
string netcoreAppRefPath = PathType.Combine(s_netcoreAppRefFolder, refAssemblyDll);
if (File.Exists(netcoreAppRefPath))
{
return netcoreAppRefPath;
}
// Look up the assembly in the framework folder. This may happen when assembly is not part of
// NetCoreApp, but comes from an additional package, such as 'Json.Net'.
string frameworkPossiblePath = PathType.Combine(s_frameworkFolder, refAssemblyDll);
if (File.Exists(frameworkPossiblePath))
{
return frameworkPossiblePath;
}
// The assembly name may point to a third-party assembly that is already loaded at run time.
if (!assembly.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
{
Assembly result = LoadAssemblyHelper(assembly);
if (result != null)
{
return result.Location;
}
}
}
// Look up the assembly in the current folder
var resolvedPaths = SessionState.Path.GetResolvedPSPathFromPSPath(refAssemblyDll);
if (resolvedPaths.Count > 0)
{
string currentFolderPath = resolvedPaths[0].Path;
if (File.Exists(currentFolderPath))
{
return currentFolderPath;
}
}
errorRecord = new ErrorRecord(
new Exception(
string.Format(ParserStrings.ErrorLoadingAssembly, assembly)),
"ErrorLoadingAssembly",
ErrorCategory.InvalidOperation,
assembly);
ThrowTerminatingError(errorRecord);
return null;
}
// LoadWithPartialName is deprecated, so we have to write the closest approximation possible.
// However, this does give us a massive usability improvement, as users can just say
// Add-Type -AssemblyName Forms (instead of System.Windows.Forms)
// This is just long, not unmaintainable.
private static Assembly LoadAssemblyHelper(string assemblyName)
{
Assembly loadedAssembly = null;
// First try by strong name
try
{
loadedAssembly = Assembly.Load(new AssemblyName(assemblyName));
}
// Generates a FileNotFoundException if you can't load the strong type.
// So we'll try from the short name.
catch (System.IO.FileNotFoundException) { }
// File load exception can happen, when we trying to load from the incorrect assembly name
// or file corrupted.
catch (System.IO.FileLoadException) { }
return loadedAssembly;
}
private IEnumerable<PortableExecutableReference> GetPortableExecutableReferences()
{
if (ReferencedAssemblies.Length > 0)
{
var tempReferences = new List<PortableExecutableReference>(s_autoReferencedAssemblies.Value);
foreach (string assembly in ReferencedAssemblies)
{
if (string.IsNullOrWhiteSpace(assembly)) { continue; }
string resolvedAssemblyPath = ResolveAssemblyName(assembly, true);
// Ignore some specified reference assemblies
string fileName = PathType.GetFileName(resolvedAssemblyPath);
if (s_refAssemblyNamesToIgnore.Value.Contains(fileName))
{
WriteVerbose(StringUtil.Format(AddTypeStrings.ReferenceAssemblyIgnored, resolvedAssemblyPath));
continue;
}
tempReferences.Add(MetadataReference.CreateFromFile(resolvedAssemblyPath));
}
return tempReferences;
}
else
{
return s_defaultAssemblies.Value;
}
}
private void WriteTypes(Assembly assembly)
{
WriteObject(assembly.GetTypes(), true);
}
#endregion LoadAssembly
#region SourceCodeProcessing
private static OutputKind OutputAssemblyTypeToOutputKind(OutputAssemblyType outputType)
{
switch (outputType)
{
case OutputAssemblyType.Library:
return OutputKind.DynamicallyLinkedLibrary;
default:
throw PSTraceSource.NewNotSupportedException();
}
}
private CommandLineArguments ParseCompilerOption(IEnumerable<string> args)
{
string sdkDirectory = s_defaultSdkDirectory;
string baseDirectory = this.SessionState.Path.CurrentLocation.Path;
switch (Language)
{
case Language.CSharp:
return CSharpCommandLineParser.Default.Parse(args, baseDirectory, sdkDirectory);
default:
throw PSTraceSource.NewNotSupportedException();
}
}
private SyntaxTree ParseSourceText(SourceText sourceText, ParseOptions parseOptions, string path = "")
{
switch (Language)
{
case Language.CSharp:
return CSharpSyntaxTree.ParseText(sourceText, (CSharpParseOptions)parseOptions, path);
default:
throw PSTraceSource.NewNotSupportedException();
}
}
private CompilationOptions GetDefaultCompilationOptions()
{
switch (Language)
{
case Language.CSharp:
return new CSharpCompilationOptions(OutputAssemblyTypeToOutputKind(OutputType));
default:
throw PSTraceSource.NewNotSupportedException();
}
}
private bool isSourceCodeUpdated(List<SyntaxTree> syntaxTrees, out Assembly assembly)
{
Diagnostics.Assert(syntaxTrees.Count != 0, "syntaxTrees should contains a source code.");
_syntaxTreesHash = SyntaxTreeArrayGetHashCode(syntaxTrees);
if (s_sourceAssemblyCache.TryGetValue(_syntaxTreesHash, out Assembly hashedAssembly))
{
assembly = hashedAssembly;
return false;
}
else
{
assembly = null;
return true;
}
}
private void SourceCodeProcessing()
{
ParseOptions parseOptions = null;
CompilationOptions compilationOptions = null;
EmitOptions emitOptions = null;
if (CompilerOptions != null)
{
var arguments = ParseCompilerOption(CompilerOptions);
HandleCompilerErrors(arguments.Errors);
parseOptions = arguments.ParseOptions;
compilationOptions = arguments.CompilationOptions.WithOutputKind(OutputAssemblyTypeToOutputKind(OutputType));
emitOptions = arguments.EmitOptions;
}
else
{
parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest);
compilationOptions = GetDefaultCompilationOptions();
}
if (!IgnoreWarnings.IsPresent)
{
compilationOptions = compilationOptions.WithGeneralDiagnosticOption(defaultDiagnosticOption);
}
SourceText sourceText;
List<SyntaxTree> syntaxTrees = new();
switch (ParameterSetName)
{
case FromPathParameterSetName:
case FromLiteralPathParameterSetName:
foreach (string filePath in _paths)
{
using (var sourceFile = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
sourceText = SourceText.From(sourceFile);
syntaxTrees.Add(ParseSourceText(sourceText, parseOptions, path: filePath));
}
}
break;
case FromMemberParameterSetName:
_sourceCode = GenerateTypeSource(Namespace, Name, _sourceCode, Language);
sourceText = SourceText.From(_sourceCode);
syntaxTrees.Add(ParseSourceText(sourceText, parseOptions));
break;
case FromSourceParameterSetName:
sourceText = SourceText.From(_sourceCode);
syntaxTrees.Add(ParseSourceText(sourceText, parseOptions));
break;
default:
Diagnostics.Assert(false, "Invalid parameter set: {0}", this.ParameterSetName);
break;
}
if (!string.IsNullOrEmpty(_outputAssembly) && !PassThru.IsPresent)
{
CompileToAssembly(syntaxTrees, compilationOptions, emitOptions);
}
else
{
// if the source code was already compiled and loaded and not changed
// we get the assembly from the cache.
if (isSourceCodeUpdated(syntaxTrees, out Assembly assembly))
{
CompileToAssembly(syntaxTrees, compilationOptions, emitOptions);
}
else
{
WriteVerbose(AddTypeStrings.AlreadyCompiledandLoaded);
if (PassThru)
{
WriteTypes(assembly);
}
}
}
}
private void CompileToAssembly(List<SyntaxTree> syntaxTrees, CompilationOptions compilationOptions, EmitOptions emitOptions)
{
IEnumerable<PortableExecutableReference> references = GetPortableExecutableReferences();
Compilation compilation = null;
switch (Language)
{
case Language.CSharp:
compilation = CSharpCompilation.Create(
PathType.GetRandomFileName(),
syntaxTrees: syntaxTrees,
references: references,
options: (CSharpCompilationOptions)compilationOptions);
break;
default:
throw PSTraceSource.NewNotSupportedException();
}
DoEmitAndLoadAssembly(compilation, emitOptions);
}
private void CheckDuplicateTypes(Compilation compilation, out ConcurrentBag<string> newTypes)
{
AllNamedTypeSymbolsVisitor visitor = new();
visitor.Visit(compilation.Assembly.GlobalNamespace);
foreach (var symbolName in visitor.DuplicateSymbols)
{
ErrorRecord errorRecord = new(
new Exception(
string.Format(AddTypeStrings.TypeAlreadyExists, symbolName)),
"TYPE_ALREADY_EXISTS",
ErrorCategory.InvalidOperation,
symbolName);
WriteError(errorRecord);
}
if (!visitor.DuplicateSymbols.IsEmpty)
{
ErrorRecord errorRecord = new(
new InvalidOperationException(AddTypeStrings.CompilerErrors),
"COMPILER_ERRORS",
ErrorCategory.InvalidData,
null);
ThrowTerminatingError(errorRecord);
}
newTypes = visitor.UniqueSymbols;
return;
}
// Visit symbols in all namespaces and collect duplicates.
private sealed class AllNamedTypeSymbolsVisitor : SymbolVisitor
{
public readonly ConcurrentBag<string> DuplicateSymbols = new();
public readonly ConcurrentBag<string> UniqueSymbols = new();
public override void VisitNamespace(INamespaceSymbol symbol)
{
// Main cycle.
// For large files we could use symbol.GetMembers().AsParallel().ForAll(s => s.Accept(this));
foreach (var member in symbol.GetMembers())
{
member.Accept(this);
}
}
public override void VisitNamedType(INamedTypeSymbol symbol)
{
// It is namespace-fully-qualified name
var symbolFullName = symbol.ToString();
if (s_sourceTypesCache.TryGetValue(symbolFullName, out _))
{
DuplicateSymbols.Add(symbolFullName);
}
else
{
UniqueSymbols.Add(symbolFullName);
}
}
}
private static void CacheNewTypes(ConcurrentBag<string> newTypes)
{
foreach (var typeName in newTypes)
{
s_sourceTypesCache.Add(typeName);
}
}
private void CacheAssembly(Assembly assembly)
{
s_sourceAssemblyCache.Add(_syntaxTreesHash, assembly);
}
private void DoEmitAndLoadAssembly(Compilation compilation, EmitOptions emitOptions)
{
EmitResult emitResult;
CheckDuplicateTypes(compilation, out ConcurrentBag<string> newTypes);
if (InMemory)
{
using (var ms = new MemoryStream())
{
emitResult = compilation.Emit(peStream: ms, options: emitOptions);
HandleCompilerErrors(emitResult.Diagnostics);
if (emitResult.Success)
{
// TODO: We could use Assembly.LoadFromStream() in future.
// See https://github.com/dotnet/corefx/issues/26994
ms.Seek(0, SeekOrigin.Begin);
Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(ms);
CacheNewTypes(newTypes);
CacheAssembly(assembly);
if (PassThru)
{
WriteTypes(assembly);
}
}
}
}
else
{
using (var fs = new FileStream(_outputAssembly, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None))
{
emitResult = compilation.Emit(peStream: fs, options: emitOptions);
}
HandleCompilerErrors(emitResult.Diagnostics);
if (emitResult.Success && PassThru)
{
Assembly assembly = Assembly.LoadFrom(_outputAssembly);
CacheNewTypes(newTypes);
CacheAssembly(assembly);
WriteTypes(assembly);
}
}
}
private void HandleCompilerErrors(ImmutableArray<Diagnostic> compilerDiagnostics)
{
if (compilerDiagnostics.Length > 0)
{
bool IsError = false;
foreach (var diagnisticRecord in compilerDiagnostics)
{
// We shouldn't specify input and output files in CompilerOptions parameter
// so suppress errors from Roslyn default command line parser:
// CS1562: Outputs without source must have the /out option specified
// CS2008: No inputs specified
// BC2008: No inputs specified
//
// On emit phase some warnings (like CS8019/BS50001) don't suppressed
// and present in diagnostic report with DefaultSeverity equal to Hidden
// so we skip them explicitly here too.
if (diagnisticRecord.IsSuppressed || diagnisticRecord.DefaultSeverity == DiagnosticSeverity.Hidden ||
string.Equals(diagnisticRecord.Id, "CS2008", StringComparison.InvariantCulture) ||
string.Equals(diagnisticRecord.Id, "CS1562", StringComparison.InvariantCulture) ||
string.Equals(diagnisticRecord.Id, "BC2008", StringComparison.InvariantCulture))
{
continue;
}
if (!IsError)
{
IsError = diagnisticRecord.Severity == DiagnosticSeverity.Error ||
(diagnisticRecord.IsWarningAsError && diagnisticRecord.Severity == DiagnosticSeverity.Warning);
}
string errorText = BuildErrorMessage(diagnisticRecord);
if (diagnisticRecord.Severity == DiagnosticSeverity.Warning)
{
WriteWarning(errorText);
}
else if (diagnisticRecord.Severity == DiagnosticSeverity.Info)
{
WriteInformation(errorText, s_writeInformationTags);
}
else
{
ErrorRecord errorRecord = new(
new Exception(errorText),
"SOURCE_CODE_ERROR",
ErrorCategory.InvalidData,
diagnisticRecord);
WriteError(errorRecord);
}
}
if (IsError)
{
ErrorRecord errorRecord = new(
new InvalidOperationException(AddTypeStrings.CompilerErrors),
"COMPILER_ERRORS",
ErrorCategory.InvalidData,
null);
ThrowTerminatingError(errorRecord);
}
}
}
private static string BuildErrorMessage(Diagnostic diagnisticRecord)
{
var location = diagnisticRecord.Location;
if (location.SourceTree == null)
{
// For some error types (linker?) we don't have related source code.
return diagnisticRecord.ToString();
}
else
{
var text = location.SourceTree.GetText();
var textLines = text.Lines;
var lineSpan = location.GetLineSpan(); // FileLinePositionSpan type.
var errorLineNumber = lineSpan.StartLinePosition.Line;
// This is typical Roslyn diagnostic message which contains
// a message number, a source context and an error position.
var diagnisticMessage = diagnisticRecord.ToString();
var errorLineString = textLines[errorLineNumber].ToString();
var errorPosition = lineSpan.StartLinePosition.Character;
StringBuilder sb = new(diagnisticMessage.Length + errorLineString.Length * 2 + 4);
sb.AppendLine(diagnisticMessage);
sb.AppendLine(errorLineString);
for (var i = 0; i < errorLineString.Length; i++)
{
if (!char.IsWhiteSpace(errorLineString[i]))
{
// We copy white chars from the source string.
sb.Append(errorLineString, 0, i);
// then pad up to the error position.
sb.Append(' ', Math.Max(0, errorPosition - i));
// then put "^" into the error position.
sb.AppendLine("^");
break;
}
}
return sb.ToString();
}
}
private static int SyntaxTreeArrayGetHashCode(IEnumerable<SyntaxTree> sts)
{
// We use our extension method EnumerableExtensions.SequenceGetHashCode<T>().
List<int> stHashes = new();
foreach (var st in sts)
{
stHashes.Add(SyntaxTreeGetHashCode(st));
}
return stHashes.SequenceGetHashCode<int>();
}
private static int SyntaxTreeGetHashCode(SyntaxTree st)
{
int hash;
if (string.IsNullOrEmpty(st.FilePath))
{
// If the file name does not exist, the source text is set by the user using parameters.
// In this case, we assume that the source text is of a small size and we can re-allocate by ToString().
hash = st.ToString().GetHashCode();
}
else
{
// If the file was modified, the write time stamp was also modified
// so we do not need to calculate the entire file hash.
var updateTime = File.GetLastWriteTimeUtc(st.FilePath);
hash = Utils.CombineHashCodes(st.FilePath.GetHashCode(), updateTime.GetHashCode());
}
return hash;
}
#endregion SourceCodeProcessing
}
}