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.
This commit is contained in:
Ilya
2018-04-25 22:40:26 -07:00
committed by Dongbo Wang
parent 63c0d8d783
commit ffa7e4bac0
6 changed files with 879 additions and 435 deletions
+4
View File
@@ -7,6 +7,9 @@
<Component Id="cmp3CC027D3F160412C9F0044EBED3115DD" Guid="{8D7CAA67-8F28-422C-85FB-BDE04902E64F}">
<File Id="filFD2EF6BC74AF459D1BB52CA1E8C6E33B" KeyPath="yes" Source="$(env.ProductSourcePath)\NJsonSchema.dll" />
</Component>
<Component Id="cmp98A11B3F71BACCA2DD0A4856577CD343" Guid="{4EFB61AC-361B-42D0-B9A1-C1FEE287CD13}">
<File Id="fil9286A659888A5C12030A9CF0B215D062" KeyPath="yes" Source="$(env.ProductSourcePath)\Microsoft.CodeAnalysis.VisualBasic.dll" />
</Component>
<Component Id="cmp85032A09199C30E63F9F2CF55B6FA9B9" Guid="{D3414CD0-7366-4008-A406-DC9E69224F42}">
<File Id="fil85032A09199C30E63F9F2CF55B6FA9B9" KeyPath="yes" Source="$(env.ProductSourcePath)\System.Memory.dll" />
</Component>
@@ -1825,6 +1828,7 @@
<Fragment>
<ComponentGroup Id="$(var.ProductDirectoryName)">
<ComponentRef Id="cmp3CC027D3F160412C9F0044EBED3115DD" />
<ComponentRef Id="cmp98A11B3F71BACCA2DD0A4856577CD343" />
<ComponentRef Id="cmp85032A09199C30E63F9F2CF55B6FA9B9" />
<ComponentRef Id="cmp12FFA2A71C4F95630792652F192DAAB3" />
<ComponentRef Id="cmp3B130879A26D2E954251BB81E8948069" />
@@ -78,6 +78,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.7.0" />
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic" Version="2.7.0" />
<PackageReference Include="NJsonSchema" Version="9.9.10" />
</ItemGroup>
File diff suppressed because it is too large Load Diff
@@ -117,8 +117,8 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="CompilationErrorFormat" xml:space="preserve">
<value>{0}({1}) : {2}</value>
<data name="AlreadyCompiledandLoaded" xml:space="preserve">
<value>The source code was already compiled and loaded.</value>
</data>
<data name="MethodDefinitionNotPublic" xml:space="preserve">
<value>The generated type defines no public methods or properties.</value>
@@ -138,9 +138,6 @@
<data name="LanguageAndProviderSpecified" xml:space="preserve">
<value>Cannot add type. Specify only the Language or CodeDomProvider parameters.</value>
</data>
<data name="SpecialNetVersionRequired" xml:space="preserve">
<value>Cannot add type. The '{0}' language requires Microsoft .NET Framework {1}.</value>
</data>
<data name="AmbiguousAssemblyName" xml:space="preserve">
<value>Cannot add type. The assembly name {0} matches both {1} and {2}.</value>
</data>
@@ -162,12 +159,6 @@
<data name="OutputTypeRequiresOutputAssembly" xml:space="preserve">
<value>Cannot add type. The OutputType parameter requires that the OutputAssembly parameter be specified.</value>
</data>
<data name="CompilerErrorWithCSC" xml:space="preserve">
<value>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.</value>
</data>
<data name="WrongCompilerParameterCombination" xml:space="preserve">
<value>Cannot add type. The '{0}' parameter and the '{1}' parameter cannot both be specified.</value>
</data>
<data name="CannotDefineNewType" xml:space="preserve">
<value>Cannot add type. Definition of new types is not supported in this language mode.</value>
</data>
@@ -36,7 +36,7 @@ namespace System.Management.Automation
yield return t;
}
internal static int SequenceGetHashCode<T>(this IEnumerable<T> xs) where T : class
internal static int SequenceGetHashCode<T>(this IEnumerable<T> xs)
{
// algorithm based on http://stackoverflow.com/questions/263400/what-is-the-best-algorithm-for-an-overridden-system-object-gethashcode
if (xs == null)
@@ -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 <sourceLanguage> 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 <sourceLanguage> 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
}
}