From ffa7e4bac0b2546c5a61fad7cd87ae353e3264dc Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 26 Apr 2018 10:40:26 +0500 Subject: [PATCH] Enhance and refactor Add-Type cmdlet (#6141) - Can compile a source from strings (TypeDefinition and MemberDefinition). - Can compile from files. - Can compile only to a file (without loading the produced assembly). - Do not recompile and don't reload if the sources have not changed. - Implement `-IgnoreWarnings` to not treat warnings as errors. By default, the cmdlet considers warnings as errors. - Add VisualBasic support. - Add new `-CompilerOptions` parameter to allow setting Roslyn command line parameters including: - Parser options. - Compile options. - Emit options. **ATTENTION:** The `CompilerOptions` can be specified along with other options like `-OutputAssembly`, `-Language` and `-IgnoreWarnings`. The explicit setting parameters will take precedence over the same settings specified in `-CompileOptions`. See docs about the compiler options: https://github.com/dotnet/roslyn/blob/master/docs/compilers/CSharp/CommandLine.md https://github.com/dotnet/roslyn/blob/master/docs/compilers/Visual%20Basic/CommandLine.md **ATTENTION:** `-OutputType` default is `Library`. If `-OutputType` is absent the `-OutputType` default overlaps a value in `CompileOptions`. In other words output type ("target" ot "t" in command line) is always ignored in `CompileOptions`. We have to use `-OutputType` to set an output type. --- assets/files.wxs | 4 + ...crosoft.PowerShell.Commands.Utility.csproj | 1 + .../commands/utility/AddType.cs | 1050 ++++++++++------- .../resources/AddTypeStrings.resx | 13 +- .../utils/ExtensionMethods.cs | 2 +- .../Add-Type.Tests.ps1 | 244 +++- 6 files changed, 879 insertions(+), 435 deletions(-) diff --git a/assets/files.wxs b/assets/files.wxs index b33e2e2fb7..8288a5c4e4 100644 --- a/assets/files.wxs +++ b/assets/files.wxs @@ -7,6 +7,9 @@ + + + @@ -1825,6 +1828,7 @@ + diff --git a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj index 1c5919dfa1..ff63666025 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj +++ b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj @@ -78,6 +78,7 @@ + diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs index 9234ba9708..3cfdae71f0 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Concurrent; using System.Collections.Immutable; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -13,19 +14,22 @@ 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 Microsoft.CodeAnalysis.VisualBasic; using PathType = System.IO.Path; namespace Microsoft.PowerShell.Commands { /// - /// Languages supported for code generation + /// Languages supported for code generation. /// [SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces")] public enum Language @@ -62,122 +66,75 @@ namespace Microsoft.PowerShell.Commands WindowsApplication } - /// - /// Compile error or warning. - /// - public class AddTypeCompilerError - { - /// - /// FileName, if compiled from paths. - /// - public string FileName { get; internal set; } - - /// - /// Line number. - /// - public int Line { get; internal set; } - - /// - /// Column number. - /// - public int Column { get; internal set; } - - /// - /// Error number code, i.e. CS0116 - /// - public string ErrorNumber { get; internal set; } - - /// - /// Error message text. - /// - public string ErrorText { get; internal set; } - - /// - /// true if warning. false if error. - /// - public bool IsWarning { get; internal set; } - } - /// /// Adds a new type to the Application Domain. /// This version is based on CodeAnalysis (Roslyn). /// - [Cmdlet(VerbsCommon.Add, "Type", DefaultParameterSetName = "FromSource", HelpUri = "https://go.microsoft.com/fwlink/?LinkID=135195")] + [Cmdlet(VerbsCommon.Add, "Type", DefaultParameterSetName = FromSourceParameterSetName, HelpUri = "https://go.microsoft.com/fwlink/?LinkID=135195")] [OutputType(typeof(Type))] public sealed class AddTypeCommand : PSCmdlet { #region Parameters /// - /// The source code of this type. + /// The source code of this generated type. /// - [Parameter(Mandatory = true, Position = 0, ParameterSetName = "FromSource")] + [Parameter(Mandatory = true, Position = 0, ParameterSetName = FromSourceParameterSetName)] public String TypeDefinition { get { - return sourceCode; + return _sourceCode; } set { - sourceCode = value; + _sourceCode = value; } } /// - /// The name of the type used for auto-generated types. + /// The name of the type (class) used for auto-generated types. /// - [Parameter(Mandatory = true, Position = 0, ParameterSetName = "FromMember")] + [Parameter(Mandatory = true, Position = 0, ParameterSetName = FromMemberParameterSetName)] public String Name { get; set; } /// - /// The source code of this method / member. + /// The source code of this generated method / member. /// - [Parameter(Mandatory = true, Position = 1, ParameterSetName = "FromMember")] + [Parameter(Mandatory = true, Position = 1, ParameterSetName = FromMemberParameterSetName)] [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] public String[] MemberDefinition { get { - return new string[] { sourceCode }; + return new string[] { _sourceCode }; } set { - sourceCode = ""; + _sourceCode = String.Empty; if (value != null) { - sourceCode = String.Join("\n", value); + _sourceCode = String.Join("\n", value); } } } - internal String sourceCode; + private String _sourceCode; /// - /// The namespaced used for the auto-generated type. + /// The namespace used for the auto-generated type. /// - [Parameter(ParameterSetName = "FromMember")] - [Alias("NS")] + [Parameter(ParameterSetName = FromMemberParameterSetName)] [AllowNull] - public String Namespace - { - get - { - return typeNamespace; - } - set - { - typeNamespace = value?.Trim(); - } - } - - internal string typeNamespace = "Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes"; + [Alias("NS")] + public String Namespace { get; set; } = "Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes"; /// /// Any using statements required by the auto-generated type. /// - [Parameter(ParameterSetName = "FromMember")] + [Parameter(ParameterSetName = FromMemberParameterSetName)] + [ValidateNotNull()] [Alias("Using")] [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] public String[] UsingNamespace { get; set; } = Utils.EmptyArray(); @@ -185,19 +142,19 @@ namespace Microsoft.PowerShell.Commands /// /// The path to the source code or DLL to load. /// - [Parameter(Mandatory = true, Position = 0, ParameterSetName = "FromPath")] + [Parameter(Mandatory = true, Position = 0, ParameterSetName = FromPathParameterSetName)] [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] public string[] Path { get { - return paths; + return _paths; } set { if (value == null) { - paths = null; + _paths = null; return; } @@ -231,20 +188,20 @@ namespace Microsoft.PowerShell.Commands /// /// The literal path to the source code or DLL to load. /// - [Parameter(Mandatory = true, ParameterSetName = "FromLiteralPath")] + [Parameter(Mandatory = true, ParameterSetName = FromLiteralPathParameterSetName)] [Alias("PSPath")] [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] public string[] LiteralPath { get { - return paths; + return _paths; } set { if (value == null) { - paths = null; + _paths = null; return; } @@ -263,7 +220,7 @@ namespace Microsoft.PowerShell.Commands { // Now, get the file type. At the same time, make sure // we aren't attempting to mix languages, as that is - // not supported by the CodeDomProvider. While it + // not supported by the Roslyn. While it // would be possible to partition the files into // languages, that would be much too complex to // describe. @@ -306,7 +263,7 @@ namespace Microsoft.PowerShell.Commands } else if (!String.Equals(activeExtension, currentExtension, StringComparison.OrdinalIgnoreCase)) { - // Throw an error if they are switching extensions + // All files must have the same extension otherwise throw. ErrorRecord errorRecord = new ErrorRecord( new Exception( StringUtil.Format(AddTypeStrings.MultipleExtensionsNotSupported)), @@ -317,48 +274,37 @@ namespace Microsoft.PowerShell.Commands ThrowTerminatingError(errorRecord); } - paths = resolvedPaths.ToArray(); + _paths = resolvedPaths.ToArray(); } } - internal string[] paths; + private string[] _paths; /// /// The name of the assembly to load. /// - [Parameter(Mandatory = true, ParameterSetName = "FromAssemblyName")] + [Parameter(Mandatory = true, ParameterSetName = FromAssemblyNameParameterSetName)] [Alias("AN")] [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] - public String[] AssemblyName - { - get - { - return assemblyNames; - } - set - { - assemblyNames = value; - loadAssembly = true; - } - } + public String[] AssemblyName { get; set; } - internal String[] assemblyNames; - internal bool loadAssembly = false; + private bool loadAssembly = false; /// - /// The language used to generate source code. + /// The language used to compile the source code. + /// Default is C#. /// - [Parameter(ParameterSetName = "FromSource")] - [Parameter(ParameterSetName = "FromMember")] + [Parameter(ParameterSetName = FromSourceParameterSetName)] + [Parameter(ParameterSetName = FromMemberParameterSetName)] public Language Language { get; set; } = Language.CSharp; /// /// Any reference DLLs to use in the compilation. /// - [Parameter(ParameterSetName = "FromSource")] - [Parameter(ParameterSetName = "FromMember")] - [Parameter(ParameterSetName = "FromPath")] - [Parameter(ParameterSetName = "FromLiteralPath")] + [Parameter(ParameterSetName = FromSourceParameterSetName)] + [Parameter(ParameterSetName = FromMemberParameterSetName)] + [Parameter(ParameterSetName = FromPathParameterSetName)] + [Parameter(ParameterSetName = FromLiteralPathParameterSetName)] [Alias("RA")] [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] public String[] ReferencedAssemblies @@ -369,29 +315,29 @@ namespace Microsoft.PowerShell.Commands if (value != null) { referencedAssemblies = value; } } } - internal string[] referencedAssemblies = Utils.EmptyArray(); + private string[] referencedAssemblies = Utils.EmptyArray(); /// /// The path to the output assembly. /// - [Parameter(ParameterSetName = "FromSource")] - [Parameter(ParameterSetName = "FromMember")] - [Parameter(ParameterSetName = "FromPath")] - [Parameter(ParameterSetName = "FromLiteralPath")] + [Parameter(ParameterSetName = FromSourceParameterSetName)] + [Parameter(ParameterSetName = FromMemberParameterSetName)] + [Parameter(ParameterSetName = FromPathParameterSetName)] + [Parameter(ParameterSetName = FromLiteralPathParameterSetName)] [Alias("OA")] public string OutputAssembly { get { - return outputAssembly; + return _outputAssembly; } set { - outputAssembly = value; + _outputAssembly = value; - if (outputAssembly != null) + if (_outputAssembly != null) { - outputAssembly = outputAssembly.Trim(); + _outputAssembly = _outputAssembly.Trim(); // Try to resolve the path ProviderInfo provider = null; @@ -399,17 +345,17 @@ namespace Microsoft.PowerShell.Commands try { - newPaths = SessionState.Path.GetResolvedProviderPathFromPSPath(outputAssembly, out provider); + newPaths = SessionState.Path.GetResolvedProviderPathFromPSPath(_outputAssembly, out provider); } // Ignore the ItemNotFound -- we handle it. catch (ItemNotFoundException) { } ErrorRecord errorRecord = new ErrorRecord( new Exception( - StringUtil.Format(AddTypeStrings.OutputAssemblyDidNotResolve, outputAssembly)), + StringUtil.Format(AddTypeStrings.OutputAssemblyDidNotResolve, _outputAssembly)), "INVALID_OUTPUT_ASSEMBLY", ErrorCategory.InvalidArgument, - outputAssembly); + _outputAssembly); // If it resolved to a non-standard provider, // generate an error. @@ -431,48 +377,35 @@ namespace Microsoft.PowerShell.Commands else if (newPaths.Count == 0) { // We can't create one with wildcard characters - if (WildcardPattern.ContainsWildcardCharacters(outputAssembly)) + if (WildcardPattern.ContainsWildcardCharacters(_outputAssembly)) { ThrowTerminatingError(errorRecord); } // Create the file else { - outputAssembly = SessionState.Path.GetUnresolvedProviderPathFromPSPath(outputAssembly); + _outputAssembly = SessionState.Path.GetUnresolvedProviderPathFromPSPath(_outputAssembly); } } // It resolved to a single file else { - outputAssembly = newPaths[0]; + _outputAssembly = newPaths[0]; } } } } - internal string outputAssembly = null; + private string _outputAssembly = null; /// /// The output type of the assembly. /// - [Parameter(ParameterSetName = "FromSource")] - [Parameter(ParameterSetName = "FromMember")] - [Parameter(ParameterSetName = "FromPath")] - [Parameter(ParameterSetName = "FromLiteralPath")] + [Parameter(ParameterSetName = FromSourceParameterSetName)] + [Parameter(ParameterSetName = FromMemberParameterSetName)] + [Parameter(ParameterSetName = FromPathParameterSetName)] + [Parameter(ParameterSetName = FromLiteralPathParameterSetName)] [Alias("OT")] - public OutputAssemblyType OutputType - { - get - { - return outputType; - } - set - { - outputTypeSpecified = true; - outputType = value; - } - } - internal OutputAssemblyType outputType = OutputAssemblyType.Library; - internal bool outputTypeSpecified = false; + public OutputAssemblyType OutputType { get; set; } = OutputAssemblyType.Library; /// /// Flag to pass the resulting types along. @@ -483,27 +416,68 @@ namespace Microsoft.PowerShell.Commands /// /// Flag to ignore warnings during compilation. /// - [Parameter()] + [Parameter(ParameterSetName = FromSourceParameterSetName)] + [Parameter(ParameterSetName = FromMemberParameterSetName)] + [Parameter(ParameterSetName = FromPathParameterSetName)] + [Parameter(ParameterSetName = FromLiteralPathParameterSetName)] public SwitchParameter IgnoreWarnings { get; set; } + /// + /// Roslyn command line parameters. + /// https://github.com/dotnet/roslyn/blob/master/docs/compilers/CSharp/CommandLine.md + /// https://github.com/dotnet/roslyn/blob/master/docs/compilers/Visual%20Basic/CommandLine.md + /// + /// Parser options: + /// langversion:string - language version from: + /// [enum]::GetNames([Microsoft.CodeAnalysis.CSharp.LanguageVersion]) + /// [enum]::GetNames([Microsoft.CodeAnalysis.VisualBasic.LanguageVersion]) + /// define:symbol list - preprocessor symbols: + /// /define:UNIX,DEBUG - CSharp + /// /define:UNIX=1,DEBUG=1 - VisualBasic + /// + /// 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 + /// imports:strings - ';'-delimited imports for VisualBasic + /// + /// 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 + /// + [Parameter(ParameterSetName = FromSourceParameterSetName)] + [Parameter(ParameterSetName = FromMemberParameterSetName)] + [Parameter(ParameterSetName = FromPathParameterSetName)] + [Parameter(ParameterSetName = FromLiteralPathParameterSetName)] + [ValidateNotNullOrEmpty] + public string[] CompilerOptions { get; set; } + #endregion Parameters #region GererateSource - internal string GenerateTypeSource(string typeNamespace, string name, string sourceCode, Language language) + private string GenerateTypeSource(string typeNamespace, string typeName, string sourceCodeText, Language language) { string usingSource = String.Format( - CultureInfo.CurrentCulture, + CultureInfo.InvariantCulture, GetUsingTemplate(language), GetUsingSet(language)); string typeSource = String.Format( - CultureInfo.CurrentCulture, - GetMethodTemplate(language), Name, sourceCode); + CultureInfo.InvariantCulture, + GetMethodTemplate(language), typeName, sourceCodeText); if (!String.IsNullOrEmpty(typeNamespace)) { return usingSource + String.Format( - CultureInfo.CurrentCulture, + CultureInfo.InvariantCulture, GetNamespaceTemplate(language), typeNamespace, typeSource); } else @@ -532,7 +506,7 @@ namespace Microsoft.PowerShell.Commands " End Class\n"; } - Diagnostics.Assert(false, "GetMethodTemplate: Unsupported language family."); + Diagnostics.Assert(false, "GetMethodTemplate: Unsupported language."); return null; } @@ -556,7 +530,7 @@ namespace Microsoft.PowerShell.Commands "End Namespace\n"; } - Diagnostics.Assert(false, "GetNamespaceTemplate: Unsupported language family."); + Diagnostics.Assert(false, "GetNamespaceTemplate: Unsupported language."); return null; } @@ -580,7 +554,7 @@ namespace Microsoft.PowerShell.Commands "\n"; } - Diagnostics.Assert(false, "GetUsingTemplate: Unsupported language family."); + Diagnostics.Assert(false, "GetUsingTemplate: Unsupported language."); return null; } @@ -605,7 +579,7 @@ namespace Microsoft.PowerShell.Commands } break; default: - Diagnostics.Assert(false, "GetUsingSet: Unsupported language family."); + Diagnostics.Assert(false, "GetUsingSet: Unsupported language."); break; } @@ -614,98 +588,10 @@ namespace Microsoft.PowerShell.Commands #endregion GererateSource - internal void HandleCompilerErrors(AddTypeCompilerError[] compilerErrors) - { - // Get the source code that corresponds to their type in the case of errors - string[] actualSource = Utils.EmptyArray(); - - // Get the source code that corresponds to the - // error if we generated it - if ((compilerErrors.Length > 0) && - (!String.Equals(ParameterSetName, "FromPath", StringComparison.OrdinalIgnoreCase)) && - (!String.Equals(ParameterSetName, "FromLiteralPath", StringComparison.OrdinalIgnoreCase)) - ) - { - actualSource = sourceCode.Split(Utils.Separators.Newline); - } - - // Write any errors to the pipeline - foreach (var error in compilerErrors) - { - OutputError(error, actualSource); - } - - if (compilerErrors.Any(e => !e.IsWarning)) - { - ErrorRecord errorRecord = new ErrorRecord( - new InvalidOperationException(AddTypeStrings.CompilerErrors), - "COMPILER_ERRORS", - ErrorCategory.InvalidData, - null); - ThrowTerminatingError(errorRecord); - } - } - - private void OutputError(AddTypeCompilerError error, string[] actualSource) - { - // Get the actual line of the file if they - // used the -FromPath parameter set - if (String.Equals(ParameterSetName, "FromPath", StringComparison.OrdinalIgnoreCase) || - String.Equals(ParameterSetName, "FromLiteralPath", StringComparison.OrdinalIgnoreCase) - ) - { - if (!String.IsNullOrEmpty(error.FileName)) - { - actualSource = System.IO.File.ReadAllLines(error.FileName); - } - } - - string errorText = StringUtil.Format(AddTypeStrings.CompilationErrorFormat, - error.FileName, error.Line, error.ErrorText) + Environment.NewLine; - - for (int lineNumber = error.Line - 1; lineNumber < error.Line + 2; lineNumber++) - { - if (lineNumber > 0) - { - if (lineNumber > actualSource.Length) - break; - - string lineText = ""; - - if (lineNumber == error.Line) - { - lineText += ">>> "; - } - - lineText += actualSource[lineNumber - 1]; - - errorText += Environment.NewLine + StringUtil.Format(AddTypeStrings.CompilationErrorFormat, - error.FileName, lineNumber, lineText) + Environment.NewLine; - } - } - - if (error.IsWarning) - { - WriteWarning(errorText); - } - else - { - ErrorRecord errorRecord = new ErrorRecord( - new Exception(errorText), - "SOURCE_CODE_ERROR", - ErrorCategory.InvalidData, - error); - - WriteError(errorRecord); - } - } - - private static Dictionary s_sourceCache = new Dictionary(); - /// - /// Generate the type(s). + /// Prevent code compilation in ConstrainedLanguage mode. /// - protected override void EndProcessing() + protected override void BeginProcessing() { // Prevent code compilation in ConstrainedLanguage mode if (SessionState.LanguageMode == PSLanguageMode.ConstrainedLanguage) @@ -714,10 +600,16 @@ namespace Microsoft.PowerShell.Commands new ErrorRecord( new PSNotSupportedException(AddTypeStrings.CannotDefineNewType), "CannotDefineNewType", ErrorCategory.PermissionDenied, null)); } + } + /// + /// Generate and load the type(s). + /// + protected override void EndProcessing() + { // Generate an error if they've specified an output // assembly type without an output assembly - if (String.IsNullOrEmpty(outputAssembly) && outputTypeSpecified) + if (String.IsNullOrEmpty(_outputAssembly) && this.MyInvocation.BoundParameters.ContainsKey(nameof(OutputType))) { ErrorRecord errorRecord = new ErrorRecord( new Exception( @@ -726,64 +618,65 @@ namespace Microsoft.PowerShell.Commands AddTypeStrings.OutputTypeRequiresOutputAssembly)), "OUTPUTTYPE_REQUIRES_ASSEMBLY", ErrorCategory.InvalidArgument, - outputType); + OutputType); ThrowTerminatingError(errorRecord); return; } + if (loadAssembly) { - if (String.Equals(ParameterSetName, "FromPath", StringComparison.OrdinalIgnoreCase) || - String.Equals(ParameterSetName, "FromLiteralPath", StringComparison.OrdinalIgnoreCase)) - { - LoadAssemblies(this.paths); - } - - if (String.Equals(ParameterSetName, "FromAssemblyName", StringComparison.OrdinalIgnoreCase)) - { - LoadAssemblies(this.assemblyNames); - } + // File extension is ".DLL" (ParameterSetName = FromPathParameterSetName or FromLiteralPathParameterSetName). + LoadAssemblies(_paths); + } + else if (ParameterSetName == FromAssemblyNameParameterSetName) + { + LoadAssemblies(AssemblyName); } else { - // Load the source if they want to load from a file - if (String.Equals(ParameterSetName, "FromPath", StringComparison.OrdinalIgnoreCase) || - String.Equals(ParameterSetName, "FromLiteralPath", StringComparison.OrdinalIgnoreCase) - ) - { - if (paths.Length == 1) - { - sourceCode = File.ReadAllText(paths[0]); - } - else - { - - // We replace 'ReadAllText' with 'StringBuilder' and 'ReadAllLines' - // to avoide temporary LOH allocations. - - StringBuilder sb = new StringBuilder(8192); - - foreach (string file in paths) - { - foreach (string line in File.ReadAllLines(file)) - { - sb.AppendLine(line); - } - } - - sourceCode = sb.ToString(); - } - } - else if (String.Equals(ParameterSetName, "FromMember", StringComparison.OrdinalIgnoreCase)) - { - sourceCode = GenerateTypeSource(typeNamespace, Name, sourceCode, Language); - } - - CompileSourceToAssembly(this.sourceCode); + // Process a source code from files or strings. + SourceCodeProcessing(); } } + #region LoadAssembly + + // We now ship the NetCoreApp2.0 reference assemblies with PowerShell Core, 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 NetCoreApp2.0 reference assemblies are located at '$PSHOME\ref'. + private static string s_netcoreAppRefFolder = PathType.Combine(PathType.GetDirectoryName(typeof(PSObject).Assembly.Location), "ref"); + private static string s_frameworkFolder = PathType.GetDirectoryName(typeof(object).Assembly.Location); + + // These assemblies are always automatically added to ReferencedAssemblies. + private static Lazy s_autoReferencedAssemblies = new Lazy(InitAutoIncludedRefAssemblies); + + // A HashSet of assembly names to be ignored if they are specified in '-ReferencedAssemblies' + private static Lazy> s_refAssemblyNamesToIgnore = new Lazy>(InitRefAssemblyNamesToIgnore); + + // These assemblies are used, when ReferencedAssemblies parameter is not specified. + private static Lazy> s_defaultAssemblies = new Lazy>(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 Dictionary s_sourceTypesCache = new Dictionary(); + private static Dictionary s_sourceAssemblyCache = new Dictionary(); + + private static readonly string s_defaultSdkDirectory = Utils.DefaultPowerShellAppBase; + private const ReportDiagnostic defaultDiagnosticOption = ReportDiagnostic.Error; + private static 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 assemblies) { foreach (string assemblyName in assemblies) @@ -803,77 +696,20 @@ namespace Microsoft.PowerShell.Commands } } - private OutputKind OutputAssemblyTypeToOutputKind(OutputAssemblyType outputType) - { - switch (outputType) - { - case OutputAssemblyType.Library: - return OutputKind.DynamicallyLinkedLibrary; - case OutputAssemblyType.ConsoleApplication: - return OutputKind.ConsoleApplication; - case OutputAssemblyType.WindowsApplication: - return OutputKind.WindowsApplication; - default: - throw new ArgumentOutOfRangeException("outputType"); - } - } - - private void CheckTypesForDuplicates(Assembly assembly) - { - foreach (var type in assembly.GetTypes()) - { - if (s_sourceCache.ContainsKey(type.FullName)) - { - if (s_sourceCache[type.FullName] != sourceCode.GetHashCode()) - { - ErrorRecord errorRecord = new ErrorRecord( - new Exception( - String.Format(AddTypeStrings.TypeAlreadyExists, type.FullName)), - "TYPE_ALREADY_EXISTS", - ErrorCategory.InvalidOperation, - type.FullName); - - ThrowTerminatingError(errorRecord); - return; - } - } - else - { - s_sourceCache[type.FullName] = sourceCode.GetHashCode(); - } - } - } - - // We now ship the NetCoreApp2.0 reference assemblies with PowerShell Core, 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 NetCoreApp2.0 reference assemblies are located at '$PSHOME\ref'. - private static string s_netcoreAppRefFolder = PathType.Combine(PathType.GetDirectoryName(typeof(PSObject).Assembly.Location), "ref"); - private static string s_frameworkFolder = PathType.GetDirectoryName(typeof(object).Assembly.Location); - - // These assemblies are always automatically added to ReferencedAssemblies. - private static Lazy s_autoReferencedAssemblies = new Lazy(InitAutoIncludedRefAssemblies); - - // A HashSet of assembly names to be ignored if they are specified in '-ReferencedAssemblies' - private static Lazy> s_refAssemblyNamesToIgnore = new Lazy>(InitRefAssemblyNamesToIgnore); - - // These assemblies are used, when ReferencedAssemblies parameter is not specified. - private static Lazy s_defaultAssemblies = new Lazy(InitDefaultRefAssemblies); - - private bool InMemory { get { return String.IsNullOrEmpty(outputAssembly); } } - /// /// Initialize the list of reference assemblies that will be used when '-ReferencedAssemblies' is not specified. /// - private static PortableExecutableReference[] InitDefaultRefAssemblies() + private static IEnumerable InitDefaultRefAssemblies() { // netcoreapp2.0 currently comes with 137 reference assemblies (maybe more in future), so we use a capacity of '150'. var defaultRefAssemblies = new List(150); + foreach (string file in Directory.EnumerateFiles(s_netcoreAppRefFolder, "*.dll", SearchOption.TopDirectoryOnly)) { defaultRefAssemblies.Add(MetadataReference.CreateFromFile(file)); } defaultRefAssemblies.Add(MetadataReference.CreateFromFile(typeof(PSObject).Assembly.Location)); - return defaultRefAssemblies.ToArray(); + return defaultRefAssemblies; } /// @@ -979,11 +815,11 @@ namespace Microsoft.PowerShell.Commands } ErrorRecord errorRecord = new ErrorRecord( - new Exception( - String.Format(ParserStrings.ErrorLoadingAssembly, assembly)), - "ErrorLoadingAssembly", - ErrorCategory.InvalidOperation, - assembly); + new Exception( + String.Format(ParserStrings.ErrorLoadingAssembly, assembly)), + "ErrorLoadingAssembly", + ErrorCategory.InvalidOperation, + assembly); ThrowTerminatingError(errorRecord); return null; @@ -1010,38 +846,11 @@ namespace Microsoft.PowerShell.Commands // or file corrupted. catch (System.IO.FileLoadException) { } - if (loadedAssembly != null) - return loadedAssembly; - - return null; + return loadedAssembly; } - private void WriteTypes(Assembly assembly) + private IEnumerable GetPortableExecutableReferences() { - WriteObject(assembly.GetTypes(), true); - } - - private void CompileSourceToAssembly(string source) - { - CSharpParseOptions parseOptions; - if (Language == Language.CSharp) - { - parseOptions = new CSharpParseOptions(); - } - else - { - ErrorRecord errorRecord = new ErrorRecord( - new Exception(String.Format(CultureInfo.CurrentCulture, AddTypeStrings.SpecialNetVersionRequired, Language.ToString(), string.Empty)), - "LANGUAGE_NOT_SUPPORTED", - ErrorCategory.InvalidArgument, - Language); - - ThrowTerminatingError(errorRecord); - parseOptions = null; - } - - SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source, parseOptions); - var references = s_defaultAssemblies.Value; if (ReferencedAssemblies.Length > 0) { var tempReferences = new List(s_autoReferencedAssemblies.Value); @@ -1059,28 +868,329 @@ namespace Microsoft.PowerShell.Commands } tempReferences.Add(MetadataReference.CreateFromFile(resolvedAssemblyPath)); } - references = tempReferences.ToArray(); + return tempReferences; + } + else + { + return s_defaultAssemblies.Value; + } + } + + private void WriteTypes(Assembly assembly) + { + WriteObject(assembly.GetTypes(), true); + } + + #endregion LoadAssembly + + #region SourceCodeProcessing + + private OutputKind OutputAssemblyTypeToOutputKind(OutputAssemblyType outputType) + { + switch (outputType) + { + case OutputAssemblyType.Library: + return OutputKind.DynamicallyLinkedLibrary; + case OutputAssemblyType.ConsoleApplication: + return OutputKind.ConsoleApplication; + case OutputAssemblyType.WindowsApplication: + return OutputKind.WindowsApplication; + default: + throw new ArgumentOutOfRangeException("outputType"); + } + } + + private CommandLineArguments ParseCompilerOption(IEnumerable args) + { + string sdkDirectory = s_defaultSdkDirectory; + string baseDirectory = this.SessionState.Path.CurrentLocation.Path; + string additionalReferenceDirectories = null; + + switch (Language) + { + case Language.CSharp: + return CSharpCommandLineParser.Default.Parse(args, baseDirectory, sdkDirectory, additionalReferenceDirectories); + case Language.VisualBasic: + return VisualBasicCommandLineParser.Default.Parse(args, baseDirectory, sdkDirectory, additionalReferenceDirectories); + default: + Diagnostics.Assert(false, "ParseCompilerOption: Unsupported language family."); + break; } - CSharpCompilation compilation = CSharpCompilation.Create( - PathType.GetRandomFileName(), - syntaxTrees: new[] { syntaxTree }, - references: references, - options: new CSharpCompilationOptions(OutputAssemblyTypeToOutputKind(OutputType))); + return null; + } + private SyntaxTree ParseSourceText(SourceText sourceText, ParseOptions parseOptions, string path = "") + { + switch (Language) + { + case Language.CSharp: + return CSharpSyntaxTree.ParseText(sourceText, (CSharpParseOptions) parseOptions, path); + + case Language.VisualBasic: + return VisualBasicSyntaxTree.ParseText(sourceText, (VisualBasicParseOptions) parseOptions, path); + + default: + Diagnostics.Assert(false, "ParseSourceText: Unsupported language family."); + break; + } + + return null; + } + + private CompilationOptions GetDefaultCompilationOptions() + { + switch (Language) + { + case Language.CSharp: + return new CSharpCompilationOptions(OutputAssemblyTypeToOutputKind(OutputType)); + + case Language.VisualBasic: + return new VisualBasicCompilationOptions(outputKind: OutputAssemblyTypeToOutputKind(OutputType)); + + default: + Diagnostics.Assert(false, "GetDefaultCompilationOptions: Unsupported language family."); + break; + } + + return null; + } + + private bool isSourceCodeUpdated(List 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 + { + compilationOptions = GetDefaultCompilationOptions(); + } + + if (!IgnoreWarnings.IsPresent) + { + compilationOptions = compilationOptions.WithGeneralDiagnosticOption(defaultDiagnosticOption); + } + + SourceText sourceText; + List syntaxTrees = new List(); + + switch (ParameterSetName) + { + case FromPathParameterSetName: + case FromLiteralPathParameterSetName: + foreach (string filePath in _paths) + { + using (var sourceFile = new FileStream(filePath, FileMode.Open)) + { + 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 syntaxTrees, CompilationOptions compilationOptions, EmitOptions emitOptions) + { + IEnumerable references = GetPortableExecutableReferences(); + Compilation compilation = null; + + switch (Language) + { + case Language.CSharp: + compilation = CSharpCompilation.Create( + PathType.GetRandomFileName(), + syntaxTrees: syntaxTrees, + references: references, + options: (CSharpCompilationOptions)compilationOptions); + break; + case Language.VisualBasic: + compilation = VisualBasicCompilation.Create( + PathType.GetRandomFileName(), + syntaxTrees: syntaxTrees, + references: references, + options: (VisualBasicCompilationOptions)compilationOptions); + break; + } + + DoEmitAndLoadAssemply(compilation, emitOptions); + } + + private void CheckDuplicateTypes(Compilation compilation, out ConcurrentBag newTypes) + { + AllNamedTypeSymbolsVisitor visitor = new AllNamedTypeSymbolsVisitor(_syntaxTreesHash); + visitor.Visit(compilation.Assembly.GlobalNamespace); + + foreach (var symbolName in visitor.DuplicateSymbols) + { + ErrorRecord errorRecord = new ErrorRecord( + new Exception( + String.Format(AddTypeStrings.TypeAlreadyExists, symbolName)), + "TYPE_ALREADY_EXISTS", + ErrorCategory.InvalidOperation, + symbolName); + WriteError(errorRecord); + } + + if (visitor.DuplicateSymbols.Count > 0) + { + ErrorRecord errorRecord = new ErrorRecord( + new InvalidOperationException(AddTypeStrings.CompilerErrors), + "COMPILER_ERRORS", + ErrorCategory.InvalidData, + null); + ThrowTerminatingError(errorRecord); + } + + newTypes = visitor.UniqueSymbols; + + return; + } + + // Visit symbols in all namespaces and collect duplicates. + private class AllNamedTypeSymbolsVisitor : SymbolVisitor + { + int _hash; + + public readonly ConcurrentBag DuplicateSymbols = new ConcurrentBag(); + public readonly ConcurrentBag UniqueSymbols = new ConcurrentBag(); + + public AllNamedTypeSymbolsVisitor(int hash) + { + _hash = hash; + } + + 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 int hash)) + { + if (hash == _hash) + { + DuplicateSymbols.Add(symbolFullName); + } + } + else + { + UniqueSymbols.Add(symbolFullName); + } + } + } + + private void CacheNewTypes(ConcurrentBag newTypes) + { + foreach (var typeName in newTypes) + { + s_sourceTypesCache.Add(typeName, _syntaxTreesHash); + } + } + + private void CacheAssemply(Assembly assembly) + { + s_sourceAssemblyCache.Add(_syntaxTreesHash, assembly); + } + + private void DoEmitAndLoadAssemply(Compilation compilation, EmitOptions emitOptions) + { EmitResult emitResult; + CheckDuplicateTypes(compilation, out ConcurrentBag newTypes); + if (InMemory) { using (var ms = new MemoryStream()) { - emitResult = compilation.Emit(ms); + emitResult = compilation.Emit(peStream: ms, options: emitOptions); + + HandleCompilerErrors(emitResult.Diagnostics); + if (emitResult.Success) { - ms.Flush(); + // TODO: We could use Assembly.LoadFromStream() in future. + // See https://github.com/dotnet/corefx/issues/26994 ms.Seek(0, SeekOrigin.Begin); - Assembly assembly = Assembly.Load(ms.ToArray()); - CheckTypesForDuplicates(assembly); + Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(ms); + + CacheNewTypes(newTypes); + CacheAssemply(assembly); + if (PassThru) { WriteTypes(assembly); @@ -1090,36 +1200,170 @@ namespace Microsoft.PowerShell.Commands } else { - emitResult = compilation.Emit(outputAssembly); - if (emitResult.Success) + using (var fs = new FileStream(_outputAssembly, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None)) { - if (PassThru) - { - Assembly assembly = Assembly.LoadFrom(outputAssembly); - CheckTypesForDuplicates(assembly); - WriteTypes(assembly); - } + emitResult = compilation.Emit(peStream: fs, options: emitOptions); + } + + HandleCompilerErrors(emitResult.Diagnostics); + + if (emitResult.Success && PassThru) + { + Assembly assembly = Assembly.LoadFrom(_outputAssembly); + + CacheNewTypes(newTypes); + CacheAssemply(assembly); + + WriteTypes(assembly); } } + } - if (emitResult.Diagnostics.Length > 0) + private void HandleCompilerErrors(ImmutableArray compilerDiagnostics) + { + if (compilerDiagnostics.Length > 0) { - HandleCompilerErrors(GetErrors(emitResult.Diagnostics)); + 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 ErrorRecord( + new Exception(errorText), + "SOURCE_CODE_ERROR", + ErrorCategory.InvalidData, + diagnisticRecord); + + WriteError(errorRecord); + } + } + + if (IsError) + { + ErrorRecord errorRecord = new ErrorRecord( + new InvalidOperationException(AddTypeStrings.CompilerErrors), + "COMPILER_ERRORS", + ErrorCategory.InvalidData, + null); + ThrowTerminatingError(errorRecord); + } } } - private AddTypeCompilerError[] GetErrors(ImmutableArray diagnostics) + private string BuildErrorMessage(Diagnostic diagnisticRecord) { - return diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error || d.Severity == DiagnosticSeverity.Warning) - .Select(d => new AddTypeCompilerError + 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 StringBuilder(diagnisticMessage.Length + errorLineString.Length * 2 + 4); + + sb.AppendLine(diagnisticMessage); + sb.AppendLine(errorLineString); + + for (var i = 0 ; i < errorLineString.Length ; i++) { - ErrorText = d.GetMessage(), - FileName = null, - Line = d.Location.GetMappedLineSpan().StartLinePosition.Line + 1, // Convert 0-based to 1-based - IsWarning = !d.IsWarningAsError && d.Severity == DiagnosticSeverity.Warning, - Column = d.Location.GetMappedLineSpan().StartLinePosition.Character + 1, // Convert 0-based to 1-based - ErrorNumber = d.Id - }).ToArray(); + 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 sts) + { + // We use our extension method EnumerableExtensions.SequenceGetHashCode(). + List stHashes = new List(); + foreach (var st in sts) + { + stHashes.Add(SyntaxTreeGetHashCode(st)); + } + + return stHashes.SequenceGetHashCode(); + } + + 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 + } } diff --git a/src/Microsoft.PowerShell.Commands.Utility/resources/AddTypeStrings.resx b/src/Microsoft.PowerShell.Commands.Utility/resources/AddTypeStrings.resx index 51762730ba..ba570831de 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/resources/AddTypeStrings.resx +++ b/src/Microsoft.PowerShell.Commands.Utility/resources/AddTypeStrings.resx @@ -117,8 +117,8 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - {0}({1}) : {2} + + The source code was already compiled and loaded. The generated type defines no public methods or properties. @@ -138,9 +138,6 @@ Cannot add type. Specify only the Language or CodeDomProvider parameters. - - Cannot add type. The '{0}' language requires Microsoft .NET Framework {1}. - Cannot add type. The assembly name {0} matches both {1} and {2}. @@ -162,12 +159,6 @@ Cannot add type. The OutputType parameter requires that the OutputAssembly parameter be specified. - - Cannot add type due to the following exception: {0}. Verify that Microsoft .NET Framework {1} is installed. On x64-based versions of Windows, you must also install the WOW64 feature. - - - Cannot add type. The '{0}' parameter and the '{1}' parameter cannot both be specified. - Cannot add type. Definition of new types is not supported in this language mode. diff --git a/src/System.Management.Automation/utils/ExtensionMethods.cs b/src/System.Management.Automation/utils/ExtensionMethods.cs index fe4d5ea908..5ed4120667 100644 --- a/src/System.Management.Automation/utils/ExtensionMethods.cs +++ b/src/System.Management.Automation/utils/ExtensionMethods.cs @@ -36,7 +36,7 @@ namespace System.Management.Automation yield return t; } - internal static int SequenceGetHashCode(this IEnumerable xs) where T : class + internal static int SequenceGetHashCode(this IEnumerable xs) { // algorithm based on http://stackoverflow.com/questions/263400/what-is-the-best-algorithm-for-an-overridden-system-object-gethashcode if (xs == null) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Add-Type.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Add-Type.Tests.ps1 index ce06916186..b43ae15b40 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Add-Type.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Add-Type.Tests.ps1 @@ -4,10 +4,10 @@ Describe "Add-Type" -Tags "CI" { BeforeAll { $guid = [Guid]::NewGuid().ToString().Replace("-","") - $code1 = @" + $CSharpCode1 = @" namespace Test.AddType { - public class BasicTest1 + public class CSharpTest1$guid { public static int Add1(int a, int b) { @@ -16,10 +16,10 @@ Describe "Add-Type" -Tags "CI" { } } "@ - $code2 = @" + $CSharpCode2 = @" namespace Test.AddType { - public class BasicTest2 + public class CSharpTest2$guid { public static int Add2(int a, int b) { @@ -28,41 +28,245 @@ Describe "Add-Type" -Tags "CI" { } } "@ - $codeFile1 = Join-Path -Path $TestDrive -ChildPath "codeFile1.cs" - $codeFile2 = Join-Path -Path $TestDrive -ChildPath "codeFile2.cs" + $CSharpFile1 = Join-Path -Path $TestDrive -ChildPath "CSharpFile1.cs" + $CSharpFile2 = Join-Path -Path $TestDrive -ChildPath "CSharpFile2.cs" - Set-Content -Path $codeFile1 -Value $code1 -Force - Set-Content -Path $codeFile2 -Value $code2 -Force + Set-Content -Path $CSharpFile1 -Value $CSharpCode1 -Force + Set-Content -Path $CSharpFile2 -Value $CSharpCode2 -Force + + $VBCode1 = @" + Namespace Test.AddType + Public Class VBTest1$guid + Public Shared Function Add1(a As Integer, b As Integer) As String + return (a + b) + End Function + End Class + End Namespace +"@ + $VBCode2 = @" + Namespace Test.AddType + Public Class VBTest2$guid + Public Shared Function Add2(a As Integer, b As Integer) As String + return (a + b) + End Function + End Class + End Namespace +"@ + $VBFile1 = Join-Path -Path $TestDrive -ChildPath "VBFile1.vb" + $VBFile2 = Join-Path -Path $TestDrive -ChildPath "VBFile2.vb" + + Set-Content -Path $VBFile1 -Value $VBCode1 -Force + Set-Content -Path $VBFile2 -Value $VBCode2 -Force + + $codeWarning = @" + namespace Test.AddType + { + public class CSharpTestWarn$guid + { + public static int Add2(int a, int b) + { + return (a + b); + } + } + } + #warning Test warning line +"@ } It "Public 'Language' enumeration contains all members" { [Enum]::GetNames("Microsoft.PowerShell.Commands.Language") -join "," | Should -BeExactly "CSharp,VisualBasic" } - It "Should not throw given a simple class definition" { - { Add-Type -TypeDefinition "public static class foo { }" } | Should -Not -Throw + It "Should not throw given a simple C# class definition" { + # Also we check that '-Language CSharp' is by default. + # In subsequent launches from the same session + # the test will be passed without real compile - it will return an assembly previously compiled. + { Add-Type -TypeDefinition "public static class CSharpfooType { }" } | Should Not Throw + [CSharpfooType].Name | Should BeExactly "CSharpfooType" + } + + It "Should not throw given a simple VisualBasic class definition" { + # In subsequent launches from the same session + # the test will be passed without real compile - it will return an assembly previously compiled. + { Add-Type -TypeDefinition "Public Class VBfooType `n End Class" -Language VisualBasic } | Should Not Throw + [VBfooType].Name | Should BeExactly "VBfooType" } It "Can use System.Management.Automation.CmdletAttribute" { $code = @" -[System.Management.Automation.Cmdlet("Get", "Thing", ConfirmImpact = System.Management.Automation.ConfirmImpact.High, SupportsPaging = true)] -public class AttributeTest$guid {} +using System.Management.Automation; +[System.Management.Automation.Cmdlet("Get", "Thing$guid", ConfirmImpact = System.Management.Automation.ConfirmImpact.High, SupportsPaging = true)] +public class AttributeTest$guid : PSCmdlet +{ + protected override void EndProcessing() + + { + WriteObject("$guid"); + } +} "@ - Add-Type -TypeDefinition $code -PassThru | Should -Not -BeNullOrEmpty + $cls = Add-Type -TypeDefinition $code -PassThru | Select-Object -First 1 + $testModule = Import-Module $cls.Assembly -PassThru + + Invoke-Expression -Command "Get-Thing$guid" | Should BeExactly $guid + + Remove-Module $testModule -ErrorAction SilentlyContinue -Force } It "Can load TPA assembly System.Runtime.Serialization.Primitives.dll" { - Add-Type -AssemblyName 'System.Runtime.Serialization.Primitives' -PassThru | Should -Not -BeNullOrEmpty + $returnedTypes = Add-Type -AssemblyName 'System.Runtime.Serialization.Primitives' -PassThru + $returnedTypes.Count | Should BeGreaterThan 0 + ($returnedTypes[0].Assembly.FullName -Split ",")[0] | Should BeExactly 'System.Runtime.Serialization.Primitives' } - It "Can compile C# files" { + It "Can compile files" -TestCases @( + @{ + type1 = "[Test.AddType.CSharpTest1$guid]" + type2 = "[Test.AddType.CSharpTest2$guid]" + file1 = $CSharpFile1 + file2 = $CSharpFile2 + sourceLanguage = "CSharp" + } + @{ + type1 = "[Test.AddType.VBTest1$guid]" + type2 = "[Test.AddType.VBTest2$guid]" + file1 = $VBFile1 + file2 = $VBFile2 + sourceLanguage = "VisualBasic" + } + ) { + param($type1, $type2, $file1, $file2, $sourceLanguage) - { [Test.AddType.BasicTest1]::Add1(1, 2) } | Should -Throw -ErrorId "TypeNotFound" - { [Test.AddType.BasicTest2]::Add2(3, 4) } | Should -Throw -ErrorId "TypeNotFound" + # The types shouldn't exist before compile the test code. + $type1 -as [type] | Should BeNullOrEmpty + $type2 -as [type] | Should BeNullOrEmpty - Add-Type -Path $codeFile1,$codeFile2 + $returnedTypes = Add-Type -Path $file1,$file2 -PassThru - { [Test.AddType.BasicTest1]::Add1(1, 2) } | Should -Not -Throw - { [Test.AddType.BasicTest2]::Add2(3, 4) } | Should -Not -Throw + $type1 = Invoke-Expression -Command $type1 + $type2 = Invoke-Expression -Command $type2 + + # We can compile, load and use new code. + $type1::Add1(1, 2) | Should Be 3 + $type2::Add2(3, 4) | Should Be 7 + + # Return the same assembly if source code has not been changed. + # Also check that '-LiteralPath' works. + $returnedTypes2 = Add-Type -LiteralPath $file1,$file2 -PassThru + $returnedTypes[0].Assembly.FullName | Should BeExactly $returnedTypes2[0].Assembly.FullName + } + + It "Can compile with MemberDefinition" -TestCases @( + @{ + sourceCode = "public static string TestString() { return UTF8Encoding.UTF8.ToString();}" + sourceType = "TestCSharpType1" + sourceNS = "TestCSharpNS" + sourceUsingNS = "System.Text" + sourceRunType = "TestCSharpNS.TestCSharpType1" + sourceDefaultNSRunType = "Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes.TestCSharpType1" + expectedResult = "System.Text.UTF8Encoding+UTF8EncodingSealed" + sourceLanguage = "CSharp" + } + @{ + sourceCode = "Public Shared Function TestString() As String `n Return UTF8Encoding.UTF8.ToString() `n End Function" + sourceType = "TestVisualBasicType1" + sourceNS = "TestVisualBasicNS" + sourceUsingNS = "System.Text" + sourceRunType = "TestVisualBasicNS.TestVisualBasicType1" + sourceDefaultNSRunType = "Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes.TestVisualBasicType1" + expectedResult = "System.Text.UTF8Encoding+UTF8EncodingSealed" + sourceLanguage = "VisualBasic" + } + ) { + param($sourceCode, $sourceType, $sourceNS, $sourceUsingNS, $sourceRunType, $sourceDefaultNSRunType, $expectedResult, $sourceLanguage) + + # Add-Type show parse and compile errors and then finish with an terminationg error. + # Catch non-termination information error. + { Add-Type -MemberDefinition $sourceCode -Name $sourceType -Namespace $sourceNS -Language $sourceLanguage -ErrorAction Stop } | Should -Throw -ErrorId "SOURCE_CODE_ERROR,Microsoft.PowerShell.Commands.AddTypeCommand" + # Catch final terminationg error. + { Add-Type -MemberDefinition $sourceCode -Name $sourceType -Namespace $sourceNS -Language $sourceLanguage -ErrorAction SilentlyContinue } | Should -Throw -ErrorId "COMPILER_ERRORS,Microsoft.PowerShell.Commands.AddTypeCommand" + + $returnedTypes = Add-Type -MemberDefinition $sourceCode -Name $sourceType -UsingNamespace $sourceUsingNS -Namespace $sourceNS -Language $sourceLanguage -PassThru + ([type]$sourceRunType)::TestString() | Should BeExactly $expectedResult + + # Return the same assembly if source code has not been changed. + $returnedTypes2 = Add-Type -MemberDefinition $sourceCode -Name $sourceType -UsingNamespace $sourceUsingNS -Namespace $sourceNS -Language $sourceLanguage -PassThru + $returnedTypes[0].Assembly.FullName | Should BeExactly $returnedTypes2[0].Assembly.FullName + + # With default namespace. + Add-Type -MemberDefinition $sourceCode -Name $sourceType -UsingNamespace $sourceUsingNS -Language $sourceLanguage + ([type]$sourceDefaultNSRunType)::TestString() | Should BeExactly $expectedResult + } + + It "Can compile without loading" { + + ## The assembly files cannot be removed once they are loaded, unless the current PowerShell session exits. + ## If we use $TestDrive here, then Pester will try to remove them afterward and result in errors. + $TempPath = [System.IO.Path]::GetTempFileName() + if (Test-Path $TempPath) { Remove-Item -Path $TempPath -Force -Recurse } + New-Item -Path $TempPath -ItemType Directory -Force > $null + + $outFile = Join-Path -Path $TempPath -ChildPath "assembly$guid.dll" + $outFile2 = Join-Path -Path $TempPath -ChildPath "assembly2$guid.dll" + + $code = @" +using System.Management.Automation; +[System.Management.Automation.Cmdlet("Get", "CompileThing$guid", ConfirmImpact = System.Management.Automation.ConfirmImpact.High, SupportsPaging = true)] +public class AttributeTest$guid : PSCmdlet +{ + protected override void EndProcessing() + + { + WriteObject("$guid"); + } +} +"@ + + $cmdlet = "Get-CompileThing$guid" + + Add-Type -TypeDefinition $code -OutputAssembly $outFile | Should -BeNullOrEmpty + # Without -PassThru we don't load output assembly + { [type]"System.Management.Automation.AttributeTest$guid" } | Should -Throw + + $outFile | Should -Exist + $types = Add-Type -TypeDefinition $code -OutputAssembly $outFile2 -PassThru + $types[0].Name | Should -BeExactly "AttributeTest$guid" + $outFile2 | Should -Exist + + { Invoke-Expression -Command $cmdlet } | Should Throw + + $testModule = Import-Module -Name $outFile -PassThru + & $cmdlet | Should BeExactly $guid + + Remove-Module $testModule -Force + } + + It "Can report C# parse and compile errors" { + # Add-Type show parse and compile errors and then finish with an terminationg error. + # We test only for '-MemberDefinition' because '-Path' uses the same code path. + # In the tests the error is that 'using System.Text;' is missing. + # + # Catch non-termination information error. + { Add-Type -MemberDefinition "public static string TestString() { return UTF8Encoding.UTF8.ToString();}" -Name "TestType1" -Namespace "TestNS" -ErrorAction Stop } | Should -Throw -ErrorId "SOURCE_CODE_ERROR,Microsoft.PowerShell.Commands.AddTypeCommand" + # Catch final terminationg error. + { Add-Type -MemberDefinition "public static string TestString() { return UTF8Encoding.UTF8.ToString();}" -Name "TestType1" -Namespace "TestNS" -ErrorAction SilentlyContinue } | Should -Throw -ErrorId "COMPILER_ERRORS,Microsoft.PowerShell.Commands.AddTypeCommand" + + # Catch non-termination information error for CompilerOptions. + { Add-Type -CompilerOptions "/platform:anycpuERROR" -Language CSharp -MemberDefinition "public static string TestString() { return ""}" -Name "TestType1" -Namespace "TestNS" -ErrorAction Stop } | Should -Throw -ErrorId "SOURCE_CODE_ERROR,Microsoft.PowerShell.Commands.AddTypeCommand" + { Add-Type -CompilerOptions "/platform:anycpuERROR" -Language VisualBasic -MemberDefinition "Public Shared Function TestString() As String `n Return `"`" `n End Function" -Name "TestType1" -Namespace "TestNS" -ErrorAction Stop } | Should -Throw -ErrorId "SOURCE_CODE_ERROR,Microsoft.PowerShell.Commands.AddTypeCommand" + } + + It "OutputType parameter requires that the OutputAssembly parameter be specified." { + $code = "public static string TestString() {}" + { Add-Type -TypeDefinition $code -OutputType Library } | Should -Throw -ErrorId "OUTPUTTYPE_REQUIRES_ASSEMBLY,Microsoft.PowerShell.Commands.AddTypeCommand" + } + + It "By default Add-Type treats 'warnings as errors'." { + { Add-Type -TypeDefinition $codeWarning -WarningAction SilentlyContinue 2>$null } | Should -Throw -ErrorId "COMPILER_ERRORS,Microsoft.PowerShell.Commands.AddTypeCommand" + } + + It "IgnoreWarnings suppress 'warnings as errors'." { + Add-Type -TypeDefinition $codeWarning -IgnoreWarnings -WarningVariable warnVar -WarningAction SilentlyContinue + $warnVar.Count | Should -Be 1 } }