Private/Get-AACPSRulePlan.ps1

function Get-AACPSRulePlan {
    <#
    .SYNOPSIS
        Works out what Invoke-AACPSRule has to read for the rules asked for -
        and refuses a -Rule that can't match anything - before any Azure call.
    .DESCRIPTION
        -Rule takes rule names or wildcards. A file path given there (a
        common slip for -RulePath) and an AAC.* name the module doesn't have
        are refused with what to use instead.
 
        When only the module's own AAC.* rules run (every -Rule starts with
        'AAC.', no -Baseline, no -RulePath), the child settings PSRule for
        Azure's rules need - diagnostic settings, blob services, API
        Management APIs, ... one to hundreds of Azure Resource Manager calls
        per resource - aren't read: the AAC rules look only at names, types
        and tags, which Resource Graph returns. -NoExpand asks for the same
        with your own rules.
 
        When the only rule left is AAC.Resource.Naming and no -ResourceType
        is given, only the types it checks are read: the table in
        PSRule\Rules\AAC.Naming.Rule.ps1 (read without running the file),
        with -Configuration's AAC_NAMING_PATTERNS added ('' removes a type).
 
        A tag rule asked for with no tags to check - none in -Configuration
        and none in Get-AACTagDefault (PSRule\Rules\AAC.Tags.Rule.ps1) -
        would silently return nothing; Notice says so, and how to name them.
 
        Returns @{ Expand; ResourceType; Read (what is read, in words);
        Notice (lines to show) }.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [string[]] $Rule = @(),
        [string[]] $ExcludeRule = @(),
        [string[]] $RulePath = @(),
        [string] $Baseline,
        [string[]] $ResourceType = @(),
        [hashtable] $Configuration = @{},
        [switch] $NoExpand
    )

    $rulesFolder = Join-Path -Path $script:AACModuleRoot -ChildPath 'PSRule/Rules'
    $known = @(Get-ChildItem -LiteralPath $rulesFolder -Filter '*.Rule.ps1' | ForEach-Object {
            [regex]::Matches((Get-Content -LiteralPath $_.FullName -Raw), "(?m)^\s*Rule\s+'([^']+)'") | ForEach-Object { $_.Groups[1].Value }
        })

    # --- -Rule: names, not files -------------------------------------------------------------
    foreach ($name in $Rule) {
        if ($name -match '[\\/]|\.(ps1|ya?ml|jsonc?)$') {
            throw "-Rule takes rule names or wildcards, such as 'AAC.Resource.Naming' or 'Azure.Storage.*' - '$name' looks like a file. The module's own rules (PSRule\Rules) always load; to add rule files of your own, pass them to -RulePath."
        }
        if ($name -like 'AAC.*' -and -not [System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($name) -and $known -notcontains $name) {
            throw "The module has no rule named '$name'. Its rules are: $(($known | Sort-Object) -join ', ')."
        }
    }

    # --- Only the module's own rules? ----------------------------------------------------------
    $aacOnly = $Rule.Count -gt 0 -and -not $Baseline -and -not $RulePath.Count -and -not @($Rule | Where-Object { $_ -notlike 'AAC.*' }).Count
    $selected = @($known | Where-Object {
            $name = $_
            (-not $Rule.Count -or @($Rule | Where-Object { $name -like $_ }).Count) -and -not @($ExcludeRule | Where-Object { $name -like $_ }).Count
        })
    $plan = @{
        Expand       = -not ($aacOnly -or $NoExpand)
        ResourceType = @($ResourceType)
        Read         = ''
        Notice       = @()
    }
    # --- Tag rules with nothing to check ---------------------------------------------------------
    $tagRules = @($selected | Where-Object { $_ -in 'AAC.Resource.RequiredTags', 'AAC.ResourceGroup.RequiredTags', 'AAC.Resource.AllowedTagValues' })
    if ($tagRules.Count -and ($Rule.Count -or -not $Configuration.Count)) {
        $tags = Get-AACRuleDefault -File 'AAC.Tags.Rule.ps1' -Function 'Get-AACTagDefault'
        $required = @(@($Configuration['AAC_REQUIRED_TAGS']) + @($tags['RequiredTags']) | Where-Object { $_ })
        $allowed = $Configuration['AAC_ALLOWED_TAG_VALUES']
        if ($null -eq $allowed -and $tags['AllowedValues'] -and $tags['AllowedValues'].Count) { $allowed = $tags['AllowedValues'] }
        $idle = @($tagRules | Where-Object { ($_ -like '*RequiredTags' -and -not $required.Count) -or ($_ -eq 'AAC.Resource.AllowedTagValues' -and $null -eq $allowed) })
        # Without -Rule, only say so when the tag rules were asked for by name - a plain run shouldn't nag.
        $named = @($idle | Where-Object { $name = $_; @($Rule | Where-Object { $name -like $_ }).Count })
        if ($named.Count) {
            $plan.Notice = @("$($named -join ', ') checked nothing: no tags are named. Name them with -Configuration @{ AAC_REQUIRED_TAGS = @('Owner', 'CostCenter'); AAC_ALLOWED_TAG_VALUES = @{ Environment = @('prod', 'test', 'dev') } }, or once for every run in Get-AACTagDefault (PSRule\Rules\AAC.Tags.Rule.ps1).")
        }
    }
    if (-not $plan.Expand) {
        $plan.Read = 'names, types and tags from Resource Graph (no child settings)'
    }

    # --- Naming alone: only the types it checks ------------------------------------------------
    if ($aacOnly -and $selected.Count -eq 1 -and $selected[0] -eq 'AAC.Resource.Naming' -and -not $ResourceType.Count) {
        $types = [ordered]@{}
        $defaults = Get-AACRuleDefault -File 'AAC.Naming.Rule.ps1' -Function 'Get-AACNamingDefault'
        foreach ($key in $defaults.Keys) { if ($defaults[$key]) { $types[([string]$key).ToLowerInvariant()] = [string]$key } }
        $overrides = $Configuration['AAC_NAMING_PATTERNS']
        if ($overrides -is [System.Collections.IDictionary]) {
            foreach ($key in $overrides.Keys) {
                $lower = ([string]$key).ToLowerInvariant()
                if ($overrides[$key]) { $types[$lower] = [string]$key } else { $types.Remove($lower) }
            }
        }
        if ($types.Count) {
            $plan.ResourceType = @($types.Values)
            $plan.Read = "the $($types.Count) resource types the naming rule checks, from Resource Graph (no child settings)"
        }
    }
    $plan
}