Indented.StubCommand.psm1

using namespace System.Management.Automation
using namespace System.Management.Automation.Internal
using namespace System.Reflection
using namespace System.Text

class ScriptBuilder {
    #
    # Properties
    #

    [String] $IndentCharacters = ' '

    Hidden [Int32] $indentCount = 0

    Hidden [String] $line = ''

    Hidden [StringBuilder] $stringBuilder = (New-Object StringBuilder)

    #
    # Public methods
    #

    [ScriptBuilder] Append([String]$String) {
        $this.line += $String

        return $this
    }

    [ScriptBuilder] AppendFormat([String]$String, [Object]$Value) {
        return $this.AppendFormat($String, @($Value))
    }

    [ScriptBuilder] AppendFormat([String]$String, [Object]$Value1, [Object]$Value2) {
        return $this.AppendFormat($String, @($Value1, $Value2))
    }

    [ScriptBuilder] AppendFormat([String]$String, [Object[]]$Values) {
        $this.line += $String -f $Values

        return $this
    }

    [ScriptBuilder] AppendLine() {
        return $this.AppendLine('')
    }

    [ScriptBuilder] AppendLine([String]$String) {
        $this.line += $String

        if ($this.line[-1] -in ')', '}' -and $this.ShouldChangeIndent()) {
            $this.indentCount--
        }

        $this.stringBuilder.AppendFormat('{0}{1}', ($this.IndentCharacters * $this.indentCount), $this.line).
                            AppendLine()

        if ($this.line[-1] -in '(', '{' -and $this.ShouldChangeIndent()) {
            $this.indentCount++
        }

        $this.line = ''

        return $this
    }

    [ScriptBuilder] AppendLines([String[]]$Lines) {
        foreach ($scriptLine in $Lines) {
            $this.AppendLine($scriptLine.Trim())
        }

        return $this
    }

    [ScriptBuilder] AppendScript([String]$Script) {
        foreach ($scriptLine in $Script -split '\r?\n') {
            $this.AppendLine($scriptLine.Trim())
        }

        return $this
    }

    [String] ToString() {
        return $this.stringBuilder.ToString()
    }

    #
    # Private methods
    #

    Hidden [Int32] CountCharacter([String]$String, [Char]$Character) {
        $count = 0
        foreach ($char in $String.GetEnumerator()) {
            if ($char -eq $Character) {
                $count++
            }
        }
        return $count
    }

    Hidden [Char] GetCompliment([Char]$Character) {
        $value = switch ($Character) {
            '('     { ')' }
            ')'     { '(' }
            '{'     { '}' }
            '}'     { '{' }
            default { $null }
        }
        return $value
    }

    Hidden [Boolean] ShouldChangeIndent() {
        if ($this.CountCharacter($this.line, $this.line[-1]) -gt $this.CountCharacter($this.line, $this.GetCompliment($this.line[-1]))) {
            return $true
        }

        return $false
    }
}

function GetTypeName {
    <#
    .SYNOPSIS
        Get the full name of a type.
    .DESCRIPTION
        Get the full name of a type.
    .NOTES
        Change log:
            31/05/2017 - Chris Dent - Created.
    #>


    [OutputType([String])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [Type]$Type
    )

    process {
        if ($Type.IsNestedPublic) {
            $Type.FullName.Replace('+', '.')
        } elseif ($Type.IsGenericType) {
            $genericTypeName = $Type.GetGenericTypeDefinition().FullName -replace '`\d+'
            '{0}<{1}>' -f $genericTypeName, (($Type.GenericTypeArguments | GetTypeName) -join ',')
        } else {
            $Type.FullName
        }
    }
}

function TestIsForeignAssembly {
    <#
    .SYNOPSIS
        Test whether or not the assembly can be considered native.
    .DESCRIPTION
        This command compares a named assembly with a list of assemblies in a text file.
 
        The comparison is used to determine whether or not a given type needs to be recreated in a stub using an empty class.
 
        The list is generated using:
 
            [AppDomain]::CurrentDomain.GetAssemblies().FullName | Sort-Object
     .NOTES
        Change log:
            07/04/2017 - Chris Dent - Improved use of script level variable.
            05/04/2017 - Chris Dent - Created.
    #>


    [OutputType([Boolean])]
    param (
        # The assembly name to test against the list.
        [String]$AssemblyName,

        # A static list of assemblies read from var\assemblyList.
        [String[]]$AssemblyList = $Script:assemblyList
    )

    if ($null -eq $AssemblyList) {
        $AssemblyList = $Script:assemblyList = Get-Content "$psscriptroot\var\assemblyList.txt"
    }

    if ($AssemblyList -contains $AssemblyName) {
        return $false
    }
    return $true
}

function Get-StubRequiredType {
    <#
    .SYNOPSIS
        Gets the list of types required by a set of commands.
    .DESCRIPTION
        The list of required types includes:
 
            Types defined for parameters attached to PowerShell commands.
            Types required to satisfy exposed public properties.
            Types required to satisfy Create or Parse methods.
 
        Type list expansion is limited to two levels. The second level of classes (not directly required by a parameter) will have type names assigned to members rewritten.
    .INPUTS
        System.Management.Automation.CommandInfo
    .NOTES
        Change log:
            11/05/2017 - Chris Dent - Created.
    #>


    [CmdletBinding()]
    [OutputType('StubTypeInfo')]
    param (
        # Resolve the list of types required by the specified command.
        [Parameter(Mandatory, Position = 1, ValueFromPipeline)]
        [CommandInfo]$CommandInfo
    )

    begin {
        $primaryTypes = @{}
    }

    process {
        foreach ($parameter in $CommandInfo.Parameters.Values) {
            if (-not $primaryTypes.Contains($parameter.ParameterType)) {
                $primaryTypes.Add($parameter.ParameterType, $null)
            }
        }
        foreach ($outputTypeAttribute in $CommandInfo.OutputType) {
            if ($outputTypeAttribute.Type -and -not $primaryTypes.Contains($outputTypeAttribute.Type)) {
                $primaryTypes.Add($outputTypeAttribute.Type, $null)
            }
        }
        # Replace array types
        $keys = $primaryTypes.Keys | ForEach-Object { $_ }
        foreach ($type in $keys) {
            if ($type.BaseType -eq ([Array])) {
                $primaryTypes.Remove($type)
                $elementType = $type.GetElementType()
                if (-not $primaryTypes.Contains($elementType)) {
                    $primaryTypes.Add($elementType, $null)
                }
            }
        }
    }

    end {
        # Remove types defined in native assemblies from the list
        $primaryTypes.Keys |
            Group-Object { $_.Assembly.FullName } |
            Where-Object { TestIsForeignAssembly $_.Name } |
            ForEach-Object { $_.Group } |
            Select-Object @{n='Type'; e={ $_ }},
                          @{n='IsPrimary'; e={ $true }} |
            Add-Member -TypeName 'StubTypeInfo' -PassThru

        # Generate a list of secondary types
        $secondaryTypes = @{}
        $primaryTypes.Keys | ForEach-Object {
            $type = $_

            $instanceMembers = $type.GetMembers([BindingFlags]'Public,Instance') |
                Where-Object MemberType -in 'Field', 'Constructor', 'Property'

            foreach ($member in $instanceMembers) {
                switch ($member.MemberType) {
                    'Field'       { $member.FieldType }
                    'Constructor' { $member.GetParameters().ParameterType }
                    'Property'    { $member.PropertyType }
                }
            }
            $staticMethods = $type.GetMethods([BindingFlags]'Public,Static') |
                Where-Object { $_.Name -in 'Create', 'Parse' -and $_.ReturnType.Name -eq $type.Name }

            foreach ($method in $staticMethods) {
                $method.GetParameters().ParameterType
            }
        } | ForEach-Object {
            if ($_.BaseType -eq [Array]) {
                $_.GetElementType()
            } elseif ($_.IsGenericType) {
                $_.GenericTypeArguments
            } else {
                $_
            }
        } | Where-Object { $_ -and -not $primaryTypes.Contains($_) -and -not $secondaryTypes.Contains($_) } | ForEach-Object {
            $secondaryTypes.Add($_, $null)
        }
        $secondaryTypes.Keys |
            Group-Object { $_.Assembly.FullName } |
            Where-Object { TestIsForeignAssembly $_.Name } |
            ForEach-Object { $_.Group } |
            Select-Object @{n='Type'; e={ $_ }},
                          @{n='IsPrimary'; e={ $false }} |
            Add-Member -TypeName 'StubTypeInfo' -PassThru
    }
}

function New-StubCommand {
    <#
    .SYNOPSIS
        Create a new partial copy of a command.
    .DESCRIPTION
        New-StubCommand recreates a command as a function with param block and dynamic param block (if used).
    .INPUTS
        System.Management.Automation.CommandInfo
    .EXAMPLE
        New-StubCommand Test-Path
 
        Create a stub of the Test-Path command.
    .EXAMPLE
        Get-Command -Module AppLocker | New-StubCommand
 
        Create a stub of all commands in the AppLocker module.
    .NOTES
        Change log:
            10/08/2019 - Johan Ljunggren - Added parameter ReplaceTypeDefinition
            10/05/2017 - Chris Dent - Added automatic help insertion.
            03/04/2017 - Chris Dent - Created.
    #>


    # This command does not change state.
    [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding(DefaultParameterSetName = 'FromPipeline')]
    [OutputType([String])]
    param (
        # Generate a stub of the specified command name.
        [Parameter(Position = 0, Mandatory, ParameterSetName = 'FromString')]
        [String]$CommandName,

        # Generate a stub of the specified command.
        [Parameter(ValueFromPipeline, ParameterSetName = 'FromPipeline')]
        [CommandInfo]$CommandInfo,

        # Request generation of type statements to satisfy parameter binding.
        [Switch]$IncludeTypeDefinition,

        # Allow population of generated stub command with a custom function body.
        [ValidateScript({
            if ($null -ne $_.Ast.ParamBlock -or $null -ne $_.Ast.DynamicParamBlock) {
                throw (New-Object ArgumentException ("FunctionBody scriptblock cannot contain Param or DynamicParam blocks"))
            } else {$true}
        })]
        [scriptblock]$FunctionBody,

        # Allow types on parameters to be replaced by another type.
        [System.Collections.Hashtable[]]$ReplaceTypeDefinition
    )

    begin {
        if ($pscmdlet.ParameterSetName -eq 'FromString') {
            $null = $psboundparameters.Remove('CommandName')
            Get-Command $CommandName | New-StubCommand @PSBoundParameters
        } else {
            $commonParameters = ([CommonParameters]).GetProperties().Name
            $shouldProcessParameters = ([ShouldProcessParameters]).GetProperties().Name
        }
    }

    process {
        if ($pscmdlet.ParameterSetName -eq 'FromPipeline') {
            try {
                $script = New-Object ScriptBuilder

                if ($IncludeTypeDefinition) {
                    $null = $script.AppendLine((Get-StubRequiredType -CommandInfo $CommandInfo | New-StubType))
                }

                $null = $script.AppendFormat('function {0} {{', $CommandInfo.Name).
                                AppendLine()

                # Write help
                $helpContent = Get-Help $CommandInfo.Name -Full
                if ($helpContent.Synopsis) {
                    $null = $script.AppendLine('<#').
                                    AppendLine('.SYNOPSIS').
                                    AppendFormat(' {0}', $helpContent.Synopsis.Trim()).
                                    AppendLine()

                    foreach ($parameter in $CommandInfo.Parameters.Keys) {
                        if ($parameter -notin $commonParameters -and $parameter -notin $shouldProcessParameters) {
                            $parameterHelp = ($helpcontent.parameters.parameter | Where-Object { $_.Name -eq $parameter }).Description.Text
                            if ($parameterHelp) {
                                $paragraphs = $parameterHelp.Split("`n", [StringSplitOptions]::RemoveEmptyEntries)

                                $null = $script.AppendFormat('.PARAMETER {0}', $parameter).
                                                AppendLine()

                                foreach ($paragraph in $paragraphs) {
                                    $null = $script.AppendFormat(' {0}', $paragraph).
                                                    AppendLine()
                                }
                            }
                        }
                    }
                    $null = $script.AppendLine('#>').
                                    AppendLine()
                }

                # Write CmdletBinding
                if ($cmdletBindingAttribute = [ProxyCommand]::GetCmdletBindingAttribute($CommandInfo)) {
                    $null = $script.AppendLine($cmdletBindingAttribute)
                }

                # Write OutputType
                foreach ($outputType in $CommandInfo.OutputType) {
                    $null = $script.Append('[OutputType(')
                    if ($outputType.Type) {
                        $null = $script.AppendFormat('[{0}]', $outputType.Type)
                    } else {
                        $null = $script.AppendFormat("'{0}'", $outputType.Name)
                    }
                    $null = $script.AppendLine(')]')
                }

                # Write param
                if ($CommandInfo.CmdletBinding -or $CommandInfo.Parameters.Count -gt 0) {
                    $null = $script.Append('param (')

                    if ($param = [ProxyCommand]::GetParamBlock($CommandInfo)) {
                        foreach ($line in $param -split '\r?\n') {
                            if ($psboundparameters.ContainsKey('ReplaceTypeDefinition')) {
                                foreach ($type in $ReplaceTypeDefinition)
                                {
                                    if ($line -match ('\[{0}\]' -f $type.ReplaceType))
                                    {
                                        $line = $line -replace $type.ReplaceType, $type.WithType
                                    }
                                }
                            }

                            $null = $script.AppendLine($line.Trim())
                        }
                    } else {
                        $null = $script.Append(' ')
                    }

                    $null = $script.AppendLine(')')
                }

                $newStubDynamicParamArguments = @{
                    CommandInfo = $CommandInfo
                }

                if ($psboundparameters.ContainsKey('ReplaceTypeDefinition')) {
                    $newStubDynamicParamArguments['ReplaceTypeDefinition'] = $ReplaceTypeDefinition
                }

                if ($dynamicParams = New-StubDynamicParam @newStubDynamicParamArguments) {
                    # Write dynamic params
                    $null = $script.AppendScript($dynamicParams)
                }

                # Insert function body, if specified
                if ($null -ne $FunctionBody) {
                    if ($null -ne $FunctionBody.Ast.BeginBlock) {
                        $null = $script.AppendLine(($FunctionBody.Ast.BeginBlock))
                    }

                    if ($null -ne $FunctionBody.Ast.ProcessBlock) {
                        $null = $script.AppendLine(($FunctionBody.Ast.ProcessBlock))
                    }

                    if ($null -ne $FunctionBody.Ast.EndBlock) {
                        if ($FunctionBody.Ast.EndBlock -imatch '\s*end\s*{') {
                            $null = $script.AppendLine(($FunctionBody.Ast.EndBlock))
                        } else {
                            # Simple scriptblock does not explicitly specify that code is in end block, so we add the block decoration
                            $null = $script.AppendLine('end {')
                            $null = $script.AppendLine(($FunctionBody.Ast.EndBlock))
                            $null = $script.AppendLine('}')
                        }
                    }
                }

                # Close the function

                $null = $script.AppendLine('}')

                $script.ToString()
            } catch {
                Write-Error -ErrorRecord $_
            }
        }
    }
}

function New-StubDynamicParam {
    <#
    .SYNOPSIS
        Creates a new script representation of a set of dynamic parameters.
    .DESCRIPTION
        Creates a new script representation of a set of dynamic parameters.
 
        The dynamic parameter set includes any attributes bound to individual parameters.
    .INPUTS
        System.Management.Automation.CommandInfo
    .EXAMPLE
        Get-Command Get-Item | New-StubDynamicParam
 
        Creates a copy of the dynamic param block used by Get-Item.
    .NOTES
        Change log:
            10/08/2019 - Johan Ljunggren - Added parameter ReplaceTypeDefinition
            04/04/2017 - Chris Dent - Created.
    #>


    # This command does not change state.
    [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    [OutputType([String])]
    param (
        # Generate a dynamic param block for the specified command.
        [Parameter(Mandatory, ValueFromPipeline)]
        [CommandInfo]$CommandInfo,

        # Allow types on parameters to be replaced by another type. Also removes any parameter attribute using the type.
        [System.Collections.Hashtable[]]$ReplaceTypeDefinition
    )

    process {
        $script = New-Object ScriptBuilder

        $dynamicParams = $CommandInfo.Parameters.Values.Where{ $_.IsDynamic }
        if ($dynamicParams.Count -gt 0) {
            $null = $script.AppendLine().
                            AppendLine('dynamicparam {').
                            AppendLine('$parameters = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary').
                            AppendLine()

            foreach ($dynamicParam in $dynamicParams) {
                $null = $script.AppendFormat('# {0}', $dynamicParam.Name).
                                AppendLine().
                                AppendLine('$attributes = New-Object System.Collections.Generic.List[Attribute]').
                                AppendLine()

                foreach ($attribute in $dynamicParam.Attributes) {
                    if ($psboundparameters.ContainsKey('ReplaceTypeDefinition')) {
                        $skipAttribute = $false

                        foreach ($type in $ReplaceTypeDefinition)
                        {
                            if ($attribute.TypeId.FullName -match $type.ReplaceType)
                            {
                                $skipAttribute = $true

                                # There can only be one type, so continuing.
                                continue
                            }
                        }

                        if ($skipAttribute)
                        {
                            continue
                        }
                    }

                    $ctor = $attribute.TypeId.GetConstructors()[0]

                    $null = $script.AppendFormat('$attribute = New-Object {0}', $attribute.TypeId.FullName)

                    $arguments = foreach ($parameter in $ctor.GetParameters()) {
                        if ($null -ne $attribute.($parameter.Name)) {
                            switch ($parameter.ParameterType) {
                                ([String])      { "'{0}'" -f $attribute.($parameter.Name) }
                                ([String[]])    { "'" + ($attribute.($parameter.Name) -join "', '") + "'" }
                                ([ScriptBlock]) { "{{{0}}}" -f $attribute.($parameter.Name) }
                                default         { $attribute.($parameter.Name) }
                            }
                        }
                    }

                    if ($null -eq $arguments) {
                        $null = $script.AppendLine()
                    } else {
                        $null = $script.AppendFormat('({0})', $arguments -join ', ').
                                        AppendLine()
                    }

                    # Parameter named parameter handler
                    if ($attribute.TypeId.Name -eq 'ParameterAttribute') {
                        $default = New-Object Parameter
                        foreach ($property in $attribute.PSObject.Properties) {
                            if ($property.Value -ne $default.($property.Name)) {
                                $value = switch ($property.TypeNameOfValue) {
                                    'System.String'  { '"{0}"' -f $property.Value }
                                    'System.Boolean' { '${0}' -f $property.Value.ToString() }
                                    default          { $property.Value }
                                }

                                $null = $script.AppendFormat('$attribute.{0} = {1}', $property.Name, $value).
                                                AppendLine()
                            }
                        }
                    }

                    # ValidatePattern named parameter handler
                    if ($attribute.TypeId.Name -eq 'ValidatePatternAttribute') {
                        if ($attribute.Options -ne 'IgnoreCase') {
                            $null = $script.AppendFormat('$attribute.Options = "{0}"', $attribute.Options.ToString()).
                                            AppendLine()
                        }
                    }

                    $null = $script.AppendLine('$attributes.Add($attribute)').
                                    AppendLine()
                }

                $parameterType = $dynamicParam.ParameterType.ToString()

                if ($psboundparameters.ContainsKey('ReplaceTypeDefinition')) {
                    foreach ($type in $ReplaceTypeDefinition)
                    {
                        if ($parameterType -match $type.ReplaceType)
                        {
                            $parameterType = $parameterType -replace $type.ReplaceType, $type.WithType

                            # There can only be one type, so continuing.
                            continue
                        }
                    }
                }

                $null = $script.AppendFormat('$parameter = New-Object System.Management.Automation.RuntimeDefinedParameter("{0}", [{1}], $attributes)', $dynamicParam.Name, $parameterType).
                                AppendLine().
                                AppendFormat('$parameters.Add("{0}", $parameter)', $dynamicParam.Name).
                                AppendLine().
                                AppendLine()
            }

            $null = $script.AppendLine('return $parameters').
                            AppendLine('}')
        }

        return $script.ToString()
    }
}

function New-StubModule {
    <#
    .SYNOPSIS
        Create a new stub module.
    .DESCRIPTION
        A stub module contains:
 
            All exported commands provided by a module.
            A copy of any enumerations used by the module from non-native assemblies.
            A stub of any .NET classes consumed by the module from non-native assemblies.
 
    .EXAMPLE
        New-StubModule -FromModule DnsClient
 
        Create stub of the DnsClient module.
 
    .EXAMPLE
        New-StubModule -FromModule ActiveDirectory -Path C:\Temp -ReplaceTypeDefinition @(
            @{
                ReplaceType = 'System\.Nullable`1\[Microsoft\.ActiveDirectory\.Management\.\w*\]'
                WithType = 'System.Object'
            },
            @{
                ReplaceType = 'Microsoft\.ActiveDirectory\.Management\.Commands\.\w*'
                WithType = 'System.Object'
            },
            @{
                ReplaceType = 'Microsoft\.ActiveDirectory\.Management\.\w*'
                WithType = 'System.Object'
            }
        )
 
        Creates a stub module of all the cmdlets and types in the module ActiveDirectory
        replacing specific types with another type. ReplaceTypeDefinition takes an
        array of hashtables. The hashtable must have two properties; the property
        ReplaceType contain the type name that should be replaced with the type
        name specified in the property WithType. The property ReplaceType supports
        regular expression.
 
    .NOTES
        Change log:
            10/08/2019 - Johan Ljunggren - Added parameter ReplaceTypeDefinition
            05/04/2017 - Chris Dent - Created.
    #>


    # This command does not change state.
    [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    [OutputType([String])]
    param (
        # The name of a module to recreate.
        [Parameter(Mandatory)]
        [String]$FromModule,

        # Save the new definition in the specified directory.
        [String]$Path,

        # Allow population of generated stub command with a custom function body. Every function in the module will have the same body.
        [ValidateScript({
            if ($null -ne $_.Ast.ParamBlock -or $null -ne $_.Ast.DynamicParamBlock) {
                throw (New-Object ArgumentException ("FunctionBody scriptblock cannot contain Param or DynamicParam blocks"))
            } else {$true}
        })]
        [ScriptBLock]$FunctionBody,

        # By default, New-StubModule uses the Module parameter of Get-Command to locate commands to stub. ForceSourceFilter makes command discovery dependent on the Source property of commands returned by Get-Command.
        [Switch]$ForceSourceFilter,

        # Allow types on parameters to be replaced by another type.
        [System.Collections.Hashtable[]]$ReplaceTypeDefinition
    )

    try {
        $erroractionpreference = 'Stop'

        if (Test-Path $FromModule) {
            $FromModule = Import-Module $FromModule -PassThru |
                Select-Object -ExpandProperty Name
        }

        # Support wildcards in the FromModule parameter.
        $GetCommandSplat = @{}
        if (-not $ForceSourceFilter) {
            $GetCommandSplat.Add('Module', $FromModule)
        }
        Get-Command @GetCommandSplat | Where-Object { -not $ForceSourceFilter -or ($ForceSourceFilter -and $_.Source -eq $FromModule) } | Group-Object Source | ForEach-Object {
            $moduleName = $_.Name

            if ($psboundparameters.ContainsKey('Path')) {
                $filePath = Join-Path $Path ('{0}.psm1' -f $moduleName)
                $null = New-Item $filePath -ItemType File -Force
            }

            # Header
            '# Name: {0}' -f $moduleName
            if (-not $ForceSourceFilter) {
                '# Version: {0}' -f (Get-Module $moduleName).Version
            }
            '# CreatedOn: {0}' -f (Get-Date -Format 'u')
            ''

            # Types
            $_.Group | Get-StubRequiredType | New-StubType

            # Commands
            $StubCommandSplat = @{}
            if ($psboundparameters.ContainsKey('FunctionBody')) {
                $StubCommandSplat = @{FunctionBody = $FunctionBody}
            }

            if ($psboundparameters.ContainsKey('ReplaceTypeDefinition')) {
                $StubCommandSplat['ReplaceTypeDefinition'] = $ReplaceTypeDefinition
            }

            $_.Group | New-StubCommand @StubCommandSplat
        } | ForEach-Object {
            if ($psboundparameters.ContainsKey('Path')) {
                $_ | Out-File $filePath -Encoding UTF8 -Append
            } else {
                $_
            }
        }
    } catch {
        throw
    }
}

function New-StubType {
    <#
    .SYNOPSIS
        Generates a class or enum definition.
    .DESCRIPTION
        Builds a type definition which represents a class or type which is used to constrain a parameter.
    .INPUTS
        System.Type
    .EXAMPLE
        New-Stubtype ([IPAddress])
 
        Create a stub representing the System.Net.IPAddress class.
    .NOTES
        Change log:
            04/04/2017 - Chris Dent - Created.
            31/05/2017 - Chris Dent - Nested type handling.
    #>


    # This command does not change state.
    [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    [OutputType([String])]
    param (
        # Generate a stub of the specified type.
        [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline)]
        [Type]$Type,

        # If a type is flagged as secondary, member types are rewritten as object to end the type dependency chain.
        [Parameter(ValueFromPipelineByPropertyName, DontShow)]
        [Boolean]$IsPrimary = $true,

        # Exclude the Add-Type wrapper statements.
        [Switch]$ExcludeAddType
    )

    begin {
        $definedStubTypes = [Ordered]@{}
    }

    process {
        if ($Type -and -not $definedStubTypes.Contains($Type)) {
            $stubType = [PSCustomObject]@{
                Type       = $Type
                Namespace  = $Type.Namespace
                Definition = $null
            }

            $script = New-Object ScriptBuilder

            #if ($Type.Namespace -ne 'System' -and $Type.Namespace) {
            # $null = $script.AppendLine().
            # AppendFormat('namespace {0}', $Type.Namespace).
            # AppendLine().
            # AppendLine('{')
            #}

            if ($Type.MemberType -eq 'NestedType') {
                if ($definedStubTypes.Contains($Type.DeclaringType)) {
                    $parentClassDefinition = $definedStubTypes[$Type.DeclaringType].Definition
                } else {
                    $parentClassDefinition = New-StubType $Type.DeclaringType -ExcludeAddType
                }

                # "Open" the parent class to allow the nested class to be injected (nested types are not automatically fabricated).
                $parentClassDefinition = $parentClassDefinition.Trim() -replace '\}$'

                $null = $script.AppendLines($parentClassDefinition.Split("`n"))
            }

            if ($Type.BaseType -eq [Enum]) {
                if ($Type.CustomAttributes.Count -gt 0 -and $Type.CustomAttributes.Where{ $_.AttributeType -eq [FlagsAttribute] }) {
                    $null = $script.AppendLine('[System.Flags]')
                }

                $underlyingType = [Enum]::GetUnderlyingType($Type)
                $typeName = switch ($underlyingType.Name) {
                    'Byte'   { 'byte' }
                    'SByte'  { 'sbyte' }
                    'Int16'  { 'short' }
                    'UInt16' { 'ushort' }
                    'Int32'  { 'int' }
                    'UInt32' { 'uint' }
                    'Int64'  { 'long' }
                    'UInt64' { 'ulong' }
                }

                $null = $script.AppendFormat('public enum {0} : {1}', $Type.Name, $typeName).
                                AppendLine().
                                AppendLine('{')

                $names = [Enum]::GetNames($Type)
                for ($i = 0; $i -lt $names.Count; $i++) {
                    $null = $script.AppendFormat(
                        '{0} = {1}',
                        $names[$i],
                        [Convert]::ChangeType(
                            [Enum]::Parse($Type, $names[$i]),
                            $underlyingType
                        )
                    )
                    if ($i -ne $values.Count - 1) {
                        $null = $script.Append(',')
                    }
                    $null = $script.AppendLine()
                }

                $null = $script.AppendLine('}')
            } else {
                $null = $script.AppendFormat('public class {0}', $Type.Name).
                                AppendLine().
                                AppendLine('{')

                if ($IsPrimary) {
                    $members = $Type.GetMembers([BindingFlags]'Public,Instance') |
                        Where-Object MemberType -in 'Field', 'Constructor', 'Property' |
                        Group-Object MemberType |
                        Sort-Object {
                            switch ($_.Name) {
                                'Field'       { 1 }
                                'Constructor' { 2 }
                                'Property'    { 3 }
                            }
                        }

                    foreach ($memberSet in $members) {
                        $null = $script.AppendFormat('// {0}', $memberSet.Name).
                                        AppendLine()

                        $null = switch ($memberSet.Name) {
                            'Field' {
                                foreach ($field in $memberSet.Group) {
                                    $script.AppendFormat('public {0} {1};', (GetTypeName $field.FieldType), $field.Name).
                                            AppendLine()
                                }
                                break
                            }
                            'Constructor' {
                                foreach ($constructor in $memberSet.Group) {
                                    $script.AppendFormat('public {0}', $Type.Name)
                                    $parameters = foreach ($parameter in $constructor.GetParameters()) {
                                        '{0} {1}' -f (GetTypeName $parameter.ParameterType), $parameter.Name
                                    }
                                    $script.AppendFormat('({0}) {{ }}', $parameters -join ', ').
                                            AppendLine()
                                }
                                break
                            }
                            'Property' {
                                foreach ($property in $memberSet.Group) {
                                    $script.AppendFormat('public {0} {1}', (GetTypeName $property.PropertyType), $property.Name).
                                            Append(' { get; set; }').
                                            AppendLine()
                                }
                                break
                            }
                        }
                        $null = $script.AppendLine()
                    }

                    # Parse and Create static methods
                    [MethodInfo[]]$methods = $Type.GetMethods([BindingFlags]'Public,Static') |
                        Where-Object { $_.Name -in 'Create', 'Parse' -and $_.ReturnType.Name -eq $Type.Name }
                    if ($methods.Count -gt 0) {
                        $null = $script.AppendLine('// Static methods')
                        foreach ($method in $methods) {
                            $null = $script.AppendFormat('public static {0} {1}', (GetTypeName $method.ReturnType), $method.Name)
                            $parameters = foreach ($parameter in $method.GetParameters()) {
                                '{0} {1}' -f (GetTypeName $parameter.ParameterType), $parameter.Name
                            }
                            $null = $script.AppendFormat('({0})', $parameters -join ', ').
                                            AppendLine().
                                            AppendLine('{').
                                            AppendFormat('return new {0}();', $Type.Name).
                                            AppendLine().
                                            AppendLine('}')
                        }
                        $null = $script.AppendLine()
                    }

                    # If the type does not implement a constructor which does not require arguments
                    if (-not $Type.GetConstructor(@())) {
                        $null = $script.AppendLine('// Fabricated constructor').
                                        AppendFormat('private {0}() {{ }}', $Type.Name).
                                        AppendLine()
                        # Add a CreateTypeInstance static method
                        $null = $script.AppendFormat('public static {0} CreateTypeInstance()', $Type.Name).
                                        AppendLine().
                                        AppendLine('{').
                                        AppendFormat('return new {0}();', $Type.Name).
                                        AppendLine().
                                        AppendLine('}')
                    }
                } else {
                    $null = $script.AppendLine('public bool IsSecondaryStubType = true;').
                                    AppendLine().
                                    AppendFormat('public {0}() {{ }}', $Type.Name).
                                    AppendLine()
                }

                $null = $script.AppendLine('}')
            }

            if ($Type.MemberType -eq 'NestedType') {
                $null = $script.AppendLine('}')
            }

            #if ($Type.Namespace -ne 'System' -and $Type.Namespace) {
            # $null = $script.AppendLine('}')
            #}

            $stubType.Definition = $script.ToString().Trim()

            if ($Type.MemberType -eq 'NestedType') {
                $definedStubTypes.($Type.DeclaringType) = $stubType
                $definedStubTypes.$Type = $null
            } else {
                $definedStubTypes.$Type = $stubType
            }
        }
    }

    end {
        if ($definedStubTypes.Count -gt 0) {
            $script = New-Object ScriptBuilder

            if (-not $ExcludeAddType) {
                $null = $script.AppendLine("Add-Type -IgnoreWarnings -TypeDefinition @'")
            }

            $definedStubTypes.Values | Group-Object Namespace | Sort-Object Name | ForEach-Object {
                if ($_.Name) {
                    $null = $script.AppendFormat('namespace {0}', $_.Name).
                                    AppendLine().
                                    AppendLine('{')
                }
                $_.Group | Sort-Object { $_.Type.FullName } | ForEach-Object {
                    $null = $script.AppendLines($_.Definition -split '\r?\n').
                                    AppendLine()
                }
                if ($_.Name) {
                    $null = $script.AppendLine('}').
                                    AppendLine()
                }
            }

            if (-not $ExcludeAddType) {
                $null = $script.AppendLine("'@")
            }

            return $script.ToString()
        }
    }
}

function InitializeModule {
    Register-ArgumentCompleter -CommandName New-StubModule -ParameterName FromModule -ScriptBlock { Get-Module | Select-Object -ExpandProperty Name }
    Register-ArgumentCompleter -CommandName New-StubCommand -ParameterName CommandName -ScriptBlock { Get-Command -CommandType Function, Cmdlet | Select-Object -ExpandProperty Name }
}

InitializeModule