mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user