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
}
}