Public/Get-PSModuleDependencies.ps1
|
<# .SYNOPSIS Analyzes external module dependencies for a PowerShell module. .DESCRIPTION Get-PSModuleDependencies identifies all external PowerShell modules that a specified module requires or uses. It discovers dependencies from multiple sources: - Explicit dependencies declared in the module manifest (RequiredModules) - Implicit dependencies found in the code (Import-Module, #requires -Module) - External commands used from non-built-in modules The function returns detailed information about each dependency including: usage count, locations where used, installation status, version compatibility, and dependency type (Required, Implicit, or Transitive). .PARAMETER ModuleName Specifies the module to analyze. Use the module name as recognized by PowerShell (for example, a loaded module or module discoverable in PSModulePath). .PARAMETER IncludeTransitiveDependencies If specified, includes transitive dependencies (dependencies of dependencies). Requires internet connectivity to query PowerShell Gallery. .PARAMETER CheckVersionCompatibility If specified, queries the PowerShell Gallery to verify version availability and compatibility. Requires internet connectivity. .PARAMETER OutputFormat Specifies the output format for the report. Valid values are: - Console (default): Formatted table output - Object: Returns PSCustomObject for pipeline processing - Json: JSON-formatted string .EXAMPLE Get-PSModuleDependencies -ModuleName PSModuleQuantityAnalyzer Analyzes PSModuleQuantityAnalyzer and returns all discovered dependencies. .EXAMPLE Get-PSModuleDependencies -ModuleName ImportExcel -CheckVersionCompatibility Analyzes ImportExcel and checks version compatibility for each dependency. .EXAMPLE Get-PSModuleDependencies -ModuleName PSModuleQuantityAnalyzer -OutputFormat Json Returns dependencies in JSON format. .OUTPUTS PSCustomObject[] when OutputFormat is 'Object' or 'Console' (default). Each object includes: ModuleName, Dependency, DependencyType, UsageCount, LocationsUsed, RequiredVersion, InstalledVersion, IsInstalled, VersionCompatible, and LastChecked properties. String in JSON format when OutputFormat is 'Json'. .NOTES The analysis relies on AST parsing for code analysis and may require elevated permissions to access module files. Transitive dependency checks require internet connectivity to query PowerShell Gallery. Some dependencies may be discovered via external command resolution, which depends on the availability of modules in the current session. .LINK https://github.com/HerrHozi/PSModuleQuantityAnalyzer #> function Get-PSModuleDependencies { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ModuleName, [switch]$IncludeTransitiveDependencies, [switch]$CheckVersionCompatibility, [ValidateSet('Console', 'Object', 'Json')] [string]$OutputFormat = 'Object' ) begin { Write-Verbose "Starting dependency analysis for module: $ModuleName" $dependencies = [System.Collections.Generic.Dictionary[string, PSObject]]::new() $moduleInfo = Get-PSModuleSourceFiles -Module $ModuleName if (-not $moduleInfo) { throw "Module '$ModuleName' not found or no source files accessible." } } process { try { # Get manifest-declared dependencies $manifestDependencies = Get-ManifestDependencies -ModuleName $ModuleName foreach ($dep in $manifestDependencies) { if (-not $dependencies.ContainsKey($dep.ModuleName)) { $dependencies[$dep.ModuleName] = $dep } } # Scan source code for implicit dependencies $codeDependencies = Get-CodeDependencies -ModuleInfo $moduleInfo foreach ($dep in $codeDependencies) { if ($dependencies.ContainsKey($dep.ModuleName)) { # Merge usage information $dependencies[$dep.ModuleName].UsageCount += $dep.UsageCount $dependencies[$dep.ModuleName].LocationsUsed += $dep.LocationsUsed $dependencies[$dep.ModuleName].DependencyType = 'Mixed (Manifest + Code)' } else { $dependencies[$dep.ModuleName] = $dep } } # Check version compatibility if requested if ($CheckVersionCompatibility) { Write-Verbose "Checking version compatibility for $($ dependencies.Count) dependencies..." $dependencies = Invoke-VersionCompatibilityCheck -Dependencies $dependencies } # Handle transitive dependencies if requested if ($IncludeTransitiveDependencies) { Write-Verbose "Analyzing transitive dependencies..." $transitiveDeps = Get-TransitiveDependencies -Dependencies $dependencies foreach ($dep in $transitiveDeps) { if (-not $dependencies.ContainsKey($dep.ModuleName)) { $dependencies[$dep.ModuleName] = $dep } } } # Format and return output $resultSet = $dependencies.Values | Sort-Object ModuleName switch ($OutputFormat) { 'Console' { Format-DependencyConsoleOutput -Dependencies $resultSet return $resultSet } 'Json' { return ($resultSet | ConvertTo-Json -Depth 3) } 'Object' { return $resultSet } } } catch { Write-Error "Error analyzing dependencies: $_" throw } } } #region Helper Functions function Get-ManifestDependencies { <# .SYNOPSIS Extracts dependencies declared in module manifest. #> [CmdletBinding()] param([string]$ModuleName) try { $moduleObject = Get-Module -Name $ModuleName -ErrorAction Stop $manifestPath = Join-Path -Path $moduleObject.ModuleBase -ChildPath "$ModuleName.psd1" if (-not (Test-Path -Path $manifestPath)) { Write-Verbose "Manifest file not found: $manifestPath" return @() } $manifest = Invoke-Expression -Command (Get-Content -Path $manifestPath -Raw) $results = @() if ($manifest.RequiredModules) { foreach ($req in $manifest.RequiredModules) { $depName = if ($req -is [hashtable]) { $req.ModuleName } else { $req } $results += [PSCustomObject]@{ ModuleName = $depName Dependency = $depName DependencyType = 'Required (Manifest)' UsageCount = 0 LocationsUsed = @() RequiredVersion = if ($req -is [hashtable]) { $req.ModuleVersion } else { 'Any' } InstalledVersion = (Get-Module -Name $depName -ListAvailable | Select-Object -First 1).Version IsInstalled = $null -ne (Get-Module -Name $depName -ListAvailable -ErrorAction SilentlyContinue) VersionCompatible = $true LastChecked = Get-Date } } } return $results } catch { Write-Verbose "Error extracting manifest dependencies: $_" return @() } } function Get-CodeDependencies { <# .SYNOPSIS Discovers dependencies from source code analysis. #> [CmdletBinding()] param([PSObject]$ModuleInfo) $results = @() $foundDependencies = @{} foreach ($sourceFile in $ModuleInfo) { try { $content = Get-Content -Path $sourceFile.FullName -Raw -ErrorAction Stop # Find Import-Module statements $importMatches = [regex]::Matches($content, 'Import-Module\s+(?<module>[\w\-]+)', 'IgnoreCase') foreach ($match in $importMatches) { $moduleName = $match.Groups['module'].Value if (-not $foundDependencies.ContainsKey($moduleName)) { $foundDependencies[$moduleName] = @{ UsageCount = 1 Locations = @($sourceFile.Name) } } else { $foundDependencies[$moduleName].UsageCount++ if ($sourceFile.Name -notin $foundDependencies[$moduleName].Locations) { $foundDependencies[$moduleName].Locations += $sourceFile.Name } } } # Find #requires -Module statements $requiresMatches = [regex]::Matches($content, '#requires\s+-Module\s+(?<module>[\w\-]+)', 'IgnoreCase') foreach ($match in $requiresMatches) { $moduleName = $match.Groups['module'].Value if (-not $foundDependencies.ContainsKey($moduleName)) { $foundDependencies[$moduleName] = @{ UsageCount = 1 Locations = @($sourceFile.Name) } } else { $foundDependencies[$moduleName].UsageCount++ if ($sourceFile.Name -notin $foundDependencies[$moduleName].Locations) { $foundDependencies[$moduleName].Locations += $sourceFile.Name } } } } catch { Write-Verbose "Error scanning file $($sourceFile.FullName): $_" } } foreach ($depName in $foundDependencies.Keys) { $results += [PSCustomObject]@{ ModuleName = $depName Dependency = $depName DependencyType = 'Implicit (Code)' UsageCount = $foundDependencies[$depName].UsageCount LocationsUsed = $foundDependencies[$depName].Locations RequiredVersion = 'Any' InstalledVersion = (Get-Module -Name $depName -ListAvailable | Select-Object -First 1).Version IsInstalled = $null -ne (Get-Module -Name $depName -ListAvailable -ErrorAction SilentlyContinue) VersionCompatible = $true LastChecked = Get-Date } } return $results } function Invoke-VersionCompatibilityCheck { <# .SYNOPSIS Validates version compatibility by checking PowerShell Gallery. #> [CmdletBinding()] param([System.Collections.Generic.Dictionary[string, PSObject]]$Dependencies) foreach ($depName in $Dependencies.Keys) { try { $galleryModule = Find-Module -Name $depName -ErrorAction SilentlyContinue if ($galleryModule) { $Dependencies[$depName].VersionCompatible = $true } else { $Dependencies[$depName].VersionCompatible = $false } } catch { Write-Verbose "Could not verify version for $depName`: $_" $Dependencies[$depName].VersionCompatible = $null } } return $Dependencies } function Get-TransitiveDependencies { <# .SYNOPSIS Recursively discovers transitive dependencies. #> [CmdletBinding()] param([System.Collections.Generic.Dictionary[string, PSObject]]$Dependencies) $results = @() $processed = [System.Collections.Generic.HashSet[string]]::new() foreach ($depName in $Dependencies.Keys) { if ($processed.Add($depName)) { try { $depModule = Get-Module -Name $depName -ListAvailable -ErrorAction Stop | Select-Object -First 1 if ($depModule) { $transitivePath = Join-Path -Path $depModule.ModuleBase -ChildPath "$depName.psd1" if (Test-Path -Path $transitivePath) { $manifest = Invoke-Expression -Command (Get-Content -Path $transitivePath -Raw) if ($manifest.RequiredModules) { foreach ($req in $manifest.RequiredModules) { $transDepName = if ($req -is [hashtable]) { $req.ModuleName } else { $req } if ($transDepName -notin $Dependencies.Keys -and $transDepName -notin $processed) { $results += [PSCustomObject]@{ ModuleName = $transDepName Dependency = $transDepName DependencyType = 'Transitive (Indirect)' UsageCount = 0 LocationsUsed = @("Dependency of $depName") RequiredVersion = if ($req -is [hashtable]) { $req.ModuleVersion } else { 'Any' } InstalledVersion = (Get-Module -Name $transDepName -ListAvailable | Select-Object -First 1).Version IsInstalled = $null -ne (Get-Module -Name $transDepName -ListAvailable -ErrorAction SilentlyContinue) VersionCompatible = $true LastChecked = Get-Date } } } } } } } catch { Write-Verbose "Error analyzing transitive dependencies for $depName`: $_" } } } return $results } function Format-DependencyConsoleOutput { <# .SYNOPSIS Formats dependency output for console display. #> [CmdletBinding()] param([PSObject[]]$Dependencies) if ($Dependencies.Count -eq 0) { Write-Host "No dependencies found." return } $Dependencies | Format-Table -Property @( @{ Name = 'Dependency'; Expression = { $_.Dependency } }, @{ Name = 'Type'; Expression = { $_.DependencyType } }, @{ Name = 'Usage'; Expression = { $_.UsageCount } }, @{ Name = 'Installed'; Expression = { $_.IsInstalled } }, @{ Name = 'Version'; Expression = { $_.InstalledVersion } }, @{ Name = 'Compatible'; Expression = { $_.VersionCompatible } } ) -AutoSize } #endregion |