883ca98dd7
* Seal private classes * Fix CS0509 * Fix CS0628
1353 lines
52 KiB
C#
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
|
|
}
|
|
}
|