Private/Generator/Resolve-OpenApiGenNameCollision.ps1

function Resolve-OpenApiGenNameCollision {
    <#
    .SYNOPSIS
        Makes command names unique without hashes and reports every rename as an OA040 warning.

    .DESCRIPTION
        The candidates (from Get-OpenApiGenCommandName) must already be sorted by path, then method.
        The first candidate with a name keeps it; every base name is reserved up front, so a rename
        never takes a name another operation would get on its own. A colliding candidate gets, in
        order of preference: a path segment that the first holder's path does not have ('{name}' ->
        'ByName', 'owners' -> 'Owner'), all such segments, the HTTP method ('Post'), and as a last
        resort a number (2, 3, ...). Names in -Reserved (for example the connection commands) are
        treated as taken. Comparisons ignore case.
        A name in -BuiltIn (the core PowerShell commands, Get-OpenApiGenBuiltInCommandName) is never
        used, so a generated module cannot shadow Get-Item or New-Item: such a candidate is renamed by
        the same rules, except that -BuiltInPrefix (the connection prefix) is tried first
        ('Get-Item' -> 'Get-ModernItem'), and the rename is an OA042 warning instead of OA040.
        Returns { Names, Findings }, where Names are the candidates in the same order with Name, Noun
        and Renamed set.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [object[]]$Candidate,

        [Parameter()]
        [AllowEmptyCollection()]
        [string[]]$Reserved = @(),

        [Parameter()]
        [AllowEmptyCollection()]
        [string[]]$BuiltIn = @(),

        [Parameter()]
        [AllowEmptyString()]
        [string]$BuiltInPrefix = ''
    )

    $comparer = [System.StringComparer]::OrdinalIgnoreCase
    $taken = New-Object -TypeName 'System.Collections.Generic.HashSet[string]' -ArgumentList $comparer
    $reservedSet = New-Object -TypeName 'System.Collections.Generic.HashSet[string]' -ArgumentList $comparer
    foreach ($name in $Reserved) {
        [void]$taken.Add($name)
        [void]$reservedSet.Add($name)
    }
    $builtInSet = New-Object -TypeName 'System.Collections.Generic.HashSet[string]' -ArgumentList $comparer
    foreach ($name in $BuiltIn) {
        [void]$taken.Add($name)
        [void]$builtInSet.Add($name)
    }
    foreach ($item in $Candidate) {
        [void]$taken.Add($item.Name)
    }

    $holders = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,object]' -ArgumentList $comparer
    $names = New-Object -TypeName System.Collections.ArrayList
    $findings = New-Object -TypeName System.Collections.ArrayList

    $segmentWords = {
        param([string]$Segment)
        $parameterMatch = [regex]::Match($Segment, '^\{(.+)\}$')
        if ($parameterMatch.Success) {
            'By'
            Split-OpenApiGenWord -Value $parameterMatch.Groups[1].Value
            return
        }
        Split-OpenApiGenWord -Value $Segment
    }
    $joinNoun = {
        param([string]$Noun, [string[]]$Words)
        $all = @($Words | Where-Object -FilterScript { $_ -ne '' })
        if ($all.Count -eq 0) {
            return $Noun
        }
        $all[$all.Count - 1] = ConvertTo-OpenApiGenSingular -Word $all[$all.Count - 1]
        return $Noun + (ConvertTo-OpenApiGenPascalCase -Word $all)
    }

    foreach ($item in $Candidate) {
        $isBuiltIn = $builtInSet.Contains($item.Name)
        $isFree = (-not $holders.ContainsKey($item.Name)) -and (-not $reservedSet.Contains($item.Name)) -and (-not $isBuiltIn)
        if ($isFree) {
            $holders[$item.Name] = $item
            [void]$names.Add(($item | Select-Object -Property * -ExcludeProperty Findings | Add-Member -NotePropertyName Renamed -NotePropertyValue $false -PassThru))
            continue
        }

        $holder = $null
        if ($holders.ContainsKey($item.Name)) {
            $holder = $holders[$item.Name]
        }
        $ownSegments = @(([string]$item.Path).Split('/') | Where-Object -FilterScript { $_ -ne '' })
        $holderSegments = @()
        if ($null -ne $holder) {
            $holderSegments = @(([string]$holder.Path).Split('/') | Where-Object -FilterScript { $_ -ne '' } | ForEach-Object -Process { $_.ToLowerInvariant() })
        }
        $distinct = @($ownSegments | Where-Object -FilterScript { $holderSegments -notcontains $_.ToLowerInvariant() })
        [array]::Reverse($distinct)

        $options = New-Object -TypeName System.Collections.ArrayList
        if ($isBuiltIn -and $BuiltInPrefix -ne '' -and -not $item.Noun.StartsWith($BuiltInPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
            [void]$options.Add($BuiltInPrefix + $item.Noun)
        }
        foreach ($segment in $distinct) {
            [void]$options.Add((& $joinNoun $item.Noun @(& $segmentWords $segment)))
        }
        if ($distinct.Count -gt 1) {
            $allWords = @()
            for ($i = $distinct.Count - 1; $i -ge 0; $i--) {
                $allWords += @(& $segmentWords $distinct[$i])
            }
            [void]$options.Add((& $joinNoun $item.Noun $allWords))
        }
        [void]$options.Add($item.Noun + (ConvertTo-OpenApiGenPascalCase -Word @($item.Method)))

        $newNoun = $null
        foreach ($option in $options) {
            if (-not $taken.Contains("$($item.Verb)-$option")) {
                $newNoun = $option
                break
            }
        }
        $number = 2
        while ($null -eq $newNoun) {
            if (-not $taken.Contains("$($item.Verb)-$($item.Noun)$number")) {
                $newNoun = "$($item.Noun)$number"
            }
            $number++
        }
        $newName = "$($item.Verb)-$newNoun"
        [void]$taken.Add($newName)
        $holders[$newName] = $item

        $other = 'a command of the generated module'
        if ($null -ne $holder) {
            $other = "operation '$($holder.OperationId)' ($($holder.Method) $($holder.Path))"
        }
        $operation = [pscustomobject]@{ OperationId = $item.OperationId; Method = $item.Method; Path = $item.Path }
        if ($isBuiltIn) {
            [void]$findings.Add((New-OpenApiGenFinding -Severity Warning -Code 'OA042' -Operation $operation -Message "Command name '$($item.Name)' for operation '$($item.OperationId)' would shadow the built-in PowerShell command of the same name; renamed to '$newName'. Use -NounPrefix (or x-ps-name) to choose the names."))
        }
        else {
            [void]$findings.Add((New-OpenApiGenFinding -Severity Warning -Code 'OA040' -Operation $operation -Message "Command name '$($item.Name)' for operation '$($item.OperationId)' collides with $other; renamed to '$newName'."))
        }

        $renamed = $item | Select-Object -Property * -ExcludeProperty Findings
        $renamed.Noun = $newNoun
        $renamed.Name = $newName
        [void]$names.Add(($renamed | Add-Member -NotePropertyName Renamed -NotePropertyValue $true -PassThru))
    }

    return [pscustomobject]@{
        Names    = $names.ToArray()
        Findings = $findings.ToArray()
    }
}