Private/Get-DscResourceAstInfo.ps1
|
function Get-DscResourceAstInfo { <# .SYNOPSIS Parses a PowerShell file using AST and extracts DSC resource class metadata including properties, methods, and enum definitions. .DESCRIPTION Uses System.Management.Automation.Language.Parser to build the AST for the supplied file. Finds every class decorated with [DscResource()] and returns property / method metadata that downstream functions need to generate manifests and JSON Schema. #> [CmdletBinding()] [OutputType([PSCustomObject[]])] param( [Parameter(Mandatory)] [string]$Path ) $tokens = $null $parseErrors = $null $resolvedPath = (Resolve-Path $Path).Path $ast = [System.Management.Automation.Language.Parser]::ParseFile( $resolvedPath, [ref]$tokens, [ref]$parseErrors ) if ($parseErrors.Count -gt 0) { # Only throw on true syntax errors, not type-resolution errors # (e.g. missing base class types that are defined elsewhere). $fatalErrors = @($parseErrors | Where-Object { $_.ErrorId -notin @('TypeNotFound', 'DscResourceMissingKeyProperty') }) if ($fatalErrors.Count -gt 0) { $msgs = $fatalErrors | ForEach-Object { $_.Message } throw "Failed to parse '$Path': $($msgs -join '; ')" } } # ── Extract .PARAMETER descriptions from comment-based help ── $parameterDescriptions = @{} $helpComment = $ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -or $node -is [System.Management.Automation.Language.ScriptBlockAst] }, $false) | ForEach-Object { if ($_.GetHelpContent()) { $_.GetHelpContent() } } | Select-Object -First 1 if (-not $helpComment) { # Try the top-level script block help $helpComment = $ast.GetHelpContent() } if ($helpComment -and $helpComment.Parameters) { foreach ($key in $helpComment.Parameters.Keys) { $desc = ($helpComment.Parameters[$key] -join ' ').Trim() if (-not [string]::IsNullOrWhiteSpace($desc)) { $parameterDescriptions[$key] = $desc } } } # Extract synopsis and description from comment-based help $helpSynopsis = if ($helpComment -and $helpComment.Synopsis) { ($helpComment.Synopsis).Trim() } else { $null } $helpDescription = if ($helpComment -and $helpComment.Description) { ($helpComment.Description).Trim() } else { $null } $enumDefs = @{} $enumAsts = $ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.TypeDefinitionAst] -and $node.IsEnum }, $true) foreach ($enumAst in $enumAsts) { $enumDefs[$enumAst.Name] = @($enumAst.Members | ForEach-Object { $_.Name }) } $classAsts = $ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.TypeDefinitionAst] -and -not $node.IsEnum -and ($node.Attributes | Where-Object { $_ -is [System.Management.Automation.Language.AttributeAst] -and $_.TypeName.Name -ieq 'DscResource' }) }, $true) foreach ($classAst in $classAsts) { $properties = [System.Collections.Generic.List[PSCustomObject]]::new() $methods = [System.Collections.Generic.List[string]]::new() foreach ($member in $classAst.Members) { if ($member -is [System.Management.Automation.Language.PropertyMemberAst]) { # Skip hidden members if ($member.IsHidden) { continue } # Must have [DscProperty()] attribute $dscAttrs = @($member.Attributes | Where-Object { $_ -is [System.Management.Automation.Language.AttributeAst] -and $_.TypeName.Name -ieq 'DscProperty' }) if ($dscAttrs.Count -eq 0) { continue } $isKey = $false $isMandatory = $false $isNotConfigurable = $false foreach ($attr in $dscAttrs) { foreach ($namedArg in $attr.NamedArguments) { # If the expression was omitted (e.g. [DscProperty(Key)]) # the implicit value is $true. $isArgTrue = $namedArg.ExpressionOmitted if (-not $isArgTrue) { try { $isArgTrue = [bool]$namedArg.Argument.SafeGetValue() } catch { $isArgTrue = $true } } if ($isArgTrue) { switch ($namedArg.ArgumentName) { 'Key' { $isKey = $true } 'Mandatory' { $isMandatory = $true } 'NotConfigurable' { $isNotConfigurable = $true } } } } } # PropertyType is a direct accessor for the type constraint # (NOT in the Attributes collection). $typeName = 'System.Object' if ($member.PropertyType) { $typeName = $member.PropertyType.TypeName.FullName } $validateSetValues = $null $validateSetAttr = $member.Attributes | Where-Object { $_ -is [System.Management.Automation.Language.AttributeAst] -and $_.TypeName.Name -ieq 'ValidateSet' } if ($validateSetAttr) { $validateSetValues = @($validateSetAttr.PositionalArguments | ForEach-Object { if ($_ -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $_.Value } else { try { $_.SafeGetValue() } catch { $_.ToString() } } }) } $enumValues = $null $typeInfo = Resolve-PropertyTypeInfo -TypeName $typeName if ($enumDefs.ContainsKey($typeInfo.BaseType)) { $enumValues = $enumDefs[$typeInfo.BaseType] } $defaultValue = $null if ($member.InitialValue) { try { $defaultValue = $member.InitialValue.SafeGetValue() } catch { <# complex default — skip #> } } $propDescription = if ($parameterDescriptions.ContainsKey($member.Name)) { $parameterDescriptions[$member.Name] } else { $null } $properties.Add([PSCustomObject]@{ Name = $member.Name TypeName = $typeName IsKey = $isKey IsMandatory = $isMandatory IsNotConfigurable = $isNotConfigurable ValidateSetValues = $validateSetValues EnumValues = $enumValues DefaultValue = $defaultValue Description = $propDescription }) } elseif ($member -is [System.Management.Automation.Language.FunctionMemberAst]) { if (-not $member.IsHidden -and $member.Name -in @('Get','Set','Test','Delete','Export')) { $methods.Add($member.Name) } } } [PSCustomObject]@{ ClassName = $classAst.Name BaseClass = if ($classAst.BaseTypes.Count -gt 0) { $classAst.BaseTypes[0].TypeName.Name } else { $null } Properties = $properties.ToArray() Methods = $methods.ToArray() SourceFile = $resolvedPath Synopsis = $helpSynopsis Description = $helpDescription } } } |