Netscoot.Core/Private/PowerShellScripts.ps1

function Find-PowerShellFiles {
    # .ps1/.psm1 beneath a root.
    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$Root)
    Get-ChildItem -LiteralPath $Root -Recurse -File -ErrorAction SilentlyContinue |
        Where-Object { $_.Extension -in '.ps1', '.psm1' -and $_.FullName -notmatch '[\\/]\.git[\\/]' }
}

function Get-PowerShellScriptReferences {
    # The file paths a script names, via the PowerShell AST: dot-source (`. path`) and call
    # (`& path`) of a .ps1, `Import-Module <path>` and `using module <path>`. Literal and
    # $PSScriptRoot-based paths resolve to Paths (Netscoot.StoredPath). A path built from other
    # variables, and any other string that looks like a path (a Join-Path argument, a
    # Publish-Module -Path value), is returned in Dynamic by its raw text so a mover can report it.
    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$File)
    $full = Resolve-FullPath $File
    $dir = Split-Path -Parent $full
    $ast = [System.Management.Automation.Language.Parser]::ParseFile($full, [ref]$null, [ref]$null)
    $stringTypes = [System.Management.Automation.Language.StringConstantExpressionAst], [System.Management.Automation.Language.ExpandableStringExpressionAst]
    $importSwitches = 'Force', 'Global', 'PassThru', 'AsCustomObject', 'NoClobber', 'DisableNameChecking', 'SkipEditionCheck', 'UseWindowsPowerShell'

    # Each candidate is the string node holding a path, and whether it must name a .ps1.
    $candidates = @()
    foreach ($c in $ast.FindAll({ param($n) $n -is [System.Management.Automation.Language.CommandAst] }, $true)) {
        $op = $c.InvocationOperator.ToString()
        if ($op -eq 'Dot' -or $op -eq 'Ampersand') {
            $first = $c.CommandElements[0]
            if ($first.GetType() -in $stringTypes) { $candidates += [pscustomobject]@{ Node = $first; ScriptOnly = $true } }
            continue
        }
        if ($c.GetCommandName() -notin 'Import-Module', 'ipmo') { continue }
        $elements = @($c.CommandElements | Select-Object -Skip 1)
        for ($i = 0; $i -lt $elements.Count; $i++) {
            $e = $elements[$i]
            if ($e -is [System.Management.Automation.Language.CommandParameterAst]) {
                if ($e.ParameterName -eq 'Name') {
                    $value = if ($e.Argument) { $e.Argument } elseif ($i + 1 -lt $elements.Count) { $elements[$i + 1] } else { $null }
                    if ($value -and $value.GetType() -in $stringTypes) { $candidates += [pscustomobject]@{ Node = $value; ScriptOnly = $false } }
                    break
                }
                if (-not $e.Argument -and $e.ParameterName -notin $importSwitches) { $i++ }
                continue
            }
            if ($e.GetType() -in $stringTypes) { $candidates += [pscustomobject]@{ Node = $e; ScriptOnly = $false } }
            break
        }
    }
    foreach ($u in $ast.UsingStatements) {
        if ($u.UsingStatementKind -eq 'Module' -and $u.Name) { $candidates += [pscustomobject]@{ Node = $u.Name; ScriptOnly = $false } }
    }

    $paths = @()
    $dynamic = @()
    $claimed = [System.Collections.Generic.HashSet[object]]::new()
    foreach ($cand in $candidates) {
        $raw = $cand.Node.Value
        [void]$claimed.Add($cand.Node)
        if ([string]::IsNullOrWhiteSpace($raw)) { continue }
        if ($cand.ScriptOnly -and $raw -notmatch '\.ps1$') { continue }
        # A bare module name (no separator, no module file extension) is resolved by PSModulePath, not a path.
        if (-not $cand.ScriptOnly -and $raw -notmatch '[\\/]' -and $raw -notmatch '\.(psd1|psm1|dll)$') { continue }
        $expanded = $raw -replace '\$PSScriptRoot', $dir
        if ($expanded -match '\$') { $dynamic += [pscustomobject]@{ File = $full; Raw = $raw }; continue }
        # Windows-style '\' separators still resolve on Unix, where '\' is otherwise a literal character.
        $expanded = $expanded.Replace('\', [System.IO.Path]::DirectorySeparatorChar)
        $abs = if ([System.IO.Path]::IsPathRooted($expanded)) { [System.IO.Path]::GetFullPath($expanded) }
               else { [System.IO.Path]::GetFullPath((Join-Path $dir $expanded)) }
        $paths += [Netscoot.StoredPath]::InScript($full, $raw, $abs.TrimEnd('\', '/'))
    }
    foreach ($s in $ast.FindAll({ param($n) $n.GetType() -in $stringTypes }, $true)) {
        if (-not $claimed.Contains($s) -and $s.Value -match '[\\/]|\.ps(1|m1|d1)$') { $dynamic += [pscustomobject]@{ File = $full; Raw = $s.Value } }
    }
    return [pscustomobject]@{ Paths = $paths; Dynamic = $dynamic }
}