Get-ExtendedModuleInfo.ps1

# Copyright (c) Matthias Wolf, Mawosoft.

using namespace System
using namespace System.Collections.Generic
using namespace System.Management.Automation
using namespace System.Management.Automation.Language

<#
.SYNOPSIS
    Gets extended informations about PowerShell script modules.
.INPUTS
    You can pipe module names or simple [PSModuleInfo] objects to this cmdlet.
.OUTPUTS
    [ExtendedModuleInfo] - Extended module info objects.
#>

function Get-ExtendedModuleInfo {
    [CmdletBinding(PositionalBinding = $false)]
    [OutputType('ExtendedModuleInfo')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'Name', ValueFromPipeline, Position = 0)]
        [SupportsWildcards()]
        [ValidateNotNullOrEmpty()]
        # Specifies names or name patterns of modules to process.
        [string[]]$Name,

        [Parameter(Mandatory, ParameterSetName = 'FullyQualifiedName', ValueFromPipeline, Position = 0)]
        [ValidateNotNullOrEmpty()]
        # Specifies module names, full module specifications, or module paths to process.
        [Microsoft.PowerShell.Commands.ModuleSpecification[]]$FullyQualifiedName,

        [Parameter(Mandatory, ParameterSetName = 'ModuleInfo', ValueFromPipeline, Position = 0)]
        [ValidateNotNullOrEmpty()]
        # Specifies module objects to process.
        [psmoduleinfo[]]$ModuleInfo,

        # Recurses and gets infos about nested modules.
        [switch]$Recurse,

        # Merges infos about a main module and its nested modules into a single [ExtendedModuleInfo].
        [switch]$Merge,

        # Parses all script files of a module to obtain additional informations.
        [switch]$Parse
    )
    begin {
        $reflector = $script:Reflector
        $resultStack = [Stack[ExtendedModuleInfo]]::new()
        $modulesSeen = [HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
        $moduleStack = [Stack[psmoduleinfo]]::new()
        $scriptFiles = [Dictionary[string, HashSet[string]]]::new($script:PathComparer)
        $emptySet = [HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
        $itemOptionsToIgnore = [ScopedItemOptions]'AllScope, Private'
        $variableNamesToIgnore = $script:BuiltInPSVariables
        if ($Merge) { $Recurse = $true }
    }
    process {
        $modules = $null
        if ($Name) {
            $modules = Get-Module -Name $Name
        }
        if ($FullyQualifiedName) {
            $modules = Get-Module -FullyQualifiedName $FullyQualifiedName
        }
        if ($ModuleInfo) {
            $modules = $ModuleInfo
        }
        foreach ($moduleItem in $modules) {
            $resultStack.Clear()
            $modulesSeen.Clear()
            $moduleStack.Clear()
            $moduleStack.Push($moduleItem)
            while ($moduleStack.Count -ne 0) {
                $module = $moduleStack.Pop()
                $key = $module.Path
                if (-not $key) { $key = $module.Name }
                if (-not $modulesSeen.Add($key)) { continue }
                if ($Recurse -and $module.NestedModules) {
                    for ($i = $module.NestedModules.Count - 1; $i -ge 0; $i--) {
                        $moduleStack.Push($module.NestedModules[$i])
                    }
                }
                if ($module.ModuleType -ne [ModuleType]::Script) { continue }
                $result = [ExtendedModuleInfo]::new()
                $result.Module = $module
                if ($module.Path -like '*.psm1') { $null = $result.ScriptFiles.Add($module.Path) }
                if ($null -ne $module.SessionState) {
                    $moduleScope = $reflector.Expose($module.SessionState).Internal.ModuleScope
                    foreach ($kvp in $moduleScope.FunctionTable.GetEnumerator()) {
                        $file = $kvp.Value.ScriptBlock.File
                        if ($file) { $null = $result.ScriptFiles.Add($file) }
                        if ($kvp.Value.Options -band $itemOptionsToIgnore) { continue }
                        $result.Functions[$kvp.Key] = $kvp.Value
                    }
                    foreach ($kvp in $moduleScope.Variables.GetEnumerator()) {
                        if ($kvp.Value.Options -band $itemOptionsToIgnore) { continue }
                        if ($kvp.Value.GetType() -ne [psvariable]) { continue }
                        if ($variableNamesToIgnore.Contains($kvp.Key)) { continue }
                        $result.Variables[$kvp.Key] = $kvp.Value
                    }
                    $typeTable = $moduleScope.TypeTable
                    if ($null -ne $typeTable) {
                        foreach ($kvp in $typeTable.GetEnumerator()) {
                            $type = $kvp.Value
                            $result.Types[$kvp.Key] = $type
                            if ($result.Assemblies.Add($type.Assembly)) {
                                foreach ($attr in $type.Assembly.GetCustomAttributes([DynamicClassImplementationAssemblyAttribute], $false)) {
                                    $file = $attr.ScriptFile
                                    if ($file) { $null = $result.ScriptFiles.Add($file) }
                                }
                            }
                        }
                    }
                }
                if ($Parse) {
                    foreach ($file in $result.ScriptFiles) {
                        $scriptvars = $null
                        if (-not $scriptFiles.TryGetValue($file, [ref]$scriptvars)) {
                            $sbast = $null
                            try {
                                $sbast = [Parser]::ParseFile($file, [ref]$null, [ref]$null)
                            }
                            catch {}
                            if ($null -eq $sbast) {
                                $scriptvars = $emptySet
                            }
                            else {
                                $varasts = $sbast.FindAll({ param($a) $a -is [VariableExpressionAst] -and $a.VariablePath.IsScript }, $true)
                                if (-not $varasts) {
                                    $scriptvars = $emptySet
                                }
                                else {
                                    $scriptvars = [HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
                                    foreach ($item in $varasts) {
                                        $null = $scriptvars.Add($item.VariablePath.UserPath.Substring(7)) # 'script:'
                                    }
                                }
                            }
                            $scriptFiles.Add($file, $scriptvars)
                        }
                        $result.ScriptVariables.UnionWith($scriptvars)
                    }
                }
                $resultStack.Push($result)
                if (-not $Merge) {
                    $resultStack.Pop()
                }
            }
            if ($resultStack.Count -ne 0) {
                $merged = [ExtendedModuleInfo]::new()
                $merged.MergedModules = [List[ExtendedModuleInfo]]::new()
                while ($resultStack.Count -ne 0) {
                    $result = $resultStack.Pop()
                    $merged.Module = $result.Module
                    $merged.MergedModules.Add($result)
                    $merged.Assemblies.UnionWith($result.Assemblies)
                    $merged.ScriptVariables.UnionWith($result.ScriptVariables)
                    foreach ($kvp in $result.Functions.GetEnumerator()) {
                        $f = $kvp.Value
                        if ($null -eq $f.Module) {
                            $reflector.Expose($f).set_Module($result.Module)
                        }
                        $merged.Functions[$kvp.Key] = $f
                    }
                    foreach ($kvp in $result.Variables.GetEnumerator()) {
                        $v = $kvp.Value
                        if ($null -eq $v.Module) {
                            $reflector.Expose($v).SetModule($result.Module)
                        }
                        $merged.Variables[$kvp.Key] = $v
                    }
                    foreach ($kvp in $result.Types.GetEnumerator()) { $merged.Types[$kvp.Key] = $kvp.Value }
                    $merged.ScriptFiles.UnionWith($result.ScriptFiles)
                }
                $merged
            }
        }
    }
}