Modules/DSCParser.psm1

function Update-DSCResultWithMetadata
{
    [CmdletBinding()]
    [OutputType([Array])]
    param(
        [Parameter(Mandatory = $true)]
        [Array]
        $Tokens,

        [Parameter(Mandatory = $true)]
        [Array]
        $ParsedObject
    )
    # Find the location of the Node token. This is to ensure
    # we only look at comments that come after.
    $i = 0
    do
    {
        $i++
    } while (($tokens[$i].Kind -ne 'DynamicKeyword' -and $tokens[$i].Extent -ne 'Node') -and $i -le $tokens.Length)
    $tokenPositionOfNode = $i

    for ($i = $tokenPositionOfNode; $i -le $tokens.Length; $i++)
    {
        $percent = ($i / ($tokens.Length - $tokenPositionOfNode) * 100)
        Write-Progress -Status "Processing $percent%" `
                       -Activity "Parsing Comments" `
                       -PercentComplete $percent
        if ($tokens[$i].Kind -eq 'Comment')
        {
            # Found a comment. Backtrack to find what resource it is part of.
            $stepback = 1
            do
            {
                $stepback++
            } while ($tokens[$i-$stepback].Kind -ne 'DynamicKeyword')

            $commentResourceType         = $tokens[$i-$stepback].Text
            $commentResourceInstanceName = $tokens[$i-$stepback + 1].Value

            # Backtrack to find what property it is associated with.
            $stepback = 1
            do
            {
                $stepback++
            } while ($tokens[$i-$stepback].Kind -ne 'Identifier')
            $commentAssociatedProperty = $tokens[$i-$stepback].Text

            # Loop through all instances in the ParsedObject to retrieve
            # the one associated with the comment.
            for ($j = 0; $j -le $ParsedObject.Length; $j++)
            {
                if ($ParsedObject[$j].ResourceName -eq $commentResourceType -and `
                    $ParsedObject[$j].ResourceInstanceName -eq $commentResourceInstanceName -and `
                    $ParsedObject[$j].Keys.Contains($commentAssociatedProperty))
                {
                    $ParsedObject[$j].Add("_metadata_$commentAssociatedProperty", $tokens[$i].Text)
                }
            }
        }
    }
    Write-Progress -Completed `
                   -Activity "Parsing Comments"
    return $ParsedObject
}

function ConvertFrom-CIMInstanceToHashtable
{
    [CMdletBinding()]
    [OutputType([system.Collections.Hashtable])]
    param(
        [Parameter(Mandatory = $true)]
        [System.Object]
        $ChildObject
    )
    # Case we have an array of CIMInstances
    if ($ChildObject.GetType().Name -eq 'PipelineAst')
    {
        $result = @()
        $statements = $ChildObject.PipelineElements.Expression.SubExpression.Statements
        foreach ($statement in $statements)
        {
            $result += ConvertFrom-CIMInstanceToHashtable -ChildObject $statement
        }
    }
    else
    {
        $result = @{}
        $KeyPairs = $ChildObject.CommandElements[2].KeyValuePairs
        $CIMInstanceName = $ChildObject.CommandElements[0].Value
        $result.Add("CIMInstance", $CIMInstanceName)
        foreach ($entry in $keyPairs)
        {
            if ($null -ne $entry.Item2.PipelineElements)
            {
                $staticType = $entry.Item2.PipelineElements.Expression.StaticType.ToString()
                $subExpression = $entry.Item2.PipelineElements.Expression.SubExpression

                if ([System.String]::IsNullOrEmpty($subExpression))
                {
                    if ([System.String]::IsNullOrEmpty($entry.Item2.PipelineElements.Expression.Value))
                    {
                        $subExpression = $entry.Item2.PipelineElements.Expression.ToString()
                    }
                    else
                    {
                        $subExpression = $entry.Item2.PipelineElements.Expression.Value
                    }
                }
            }
            elseif ($null -ne $entry.Item2.CommandElements)
            {
                $staticType    = $entry.Item2.CommandElements[2].StaticType.ToString()
                $subExpression = $entry.Item2.CommandElements[0].Value
            }

            # Case where the item is an array of Sub-CIMInstances.
            if ($staticType -eq 'System.Object[]' -and `
                $subExpression.ToString().StartsWith('MSFT_'))
            {
                $subResult = @()
                foreach ($subItem in $subExpression)
                {
                    $subResult += ConvertFrom-CIMInstanceToHashtable -ChildObject $subItem.Statements
                }
                $result.Add($entry.Item1.ToString(), $subResult)
            }
            # Case the item is a single CIMInstance.
            elseif ($staticType -eq 'System.Collections.Hashtable' -and `
                $subExpression.ToString().StartsWith('MSFT_'))
            {
                $subResult = ConvertFrom-CIMInstanceToHashtable -ChildObject $entry.Item2
                $result.Add($entry.Item1.ToString(), $subResult)
            }
            else
            {
                $result.Add($entry.Item1.ToString(), $subExpression)
            }
        }
    }

    return $result
}

function ConvertTo-DSCObject
{
    [CmdletBinding(DefaultParameterSetName = 'Path')]
    [OutputType([Array])]
    param
    (
        [Parameter(Mandatory = $true,
            ParameterSetName = 'Path')]
        [ValidateScript({
                if (-Not ($_ | Test-Path) ) {
                    throw "File or folder does not exist"
                }
                if (-Not ($_ | Test-Path -PathType Leaf) ) {
                    throw "The Path argument must be a file. Folder paths are not allowed."
                }
                return $true
            })]
        [System.String]
        $Path,

        [Parameter(Mandatory = $true,
            ParameterSetName = 'Content')]
        [System.String]
        $Content,

        [Parameter(ParameterSetName = 'Path')]
        [Parameter(ParameterSetName = 'Content')]
        [System.Boolean]
        $IncludeComments = $false
    )
    $result = @()
    $Tokens      = $null
    $ParseErrors = $null

    # Retrieve information about the resources in the Microsoft365DSC
    # Module for schema and property type validation.
    $DSCResources = Get-DSCResource -Module 'Microsoft365DSC'

    # Use the AST to parse the DSC configuration
    if (-not [System.String]::IsNullOrEmpty($Path))
    {
        $AST = [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $Path), [ref]$Tokens, [ref]$ParseErrors)
    }
    else
    {
        $AST = [System.Management.Automation.Language.Parser]::ParseInput($Content, [ref]$Tokens, [ref]$ParseErrors)
    }
    # Look up the Configuration definition ("")
    $Config = $AST.Find({$Args[0].GetType().Name -eq 'ConfigurationDefinitionAst'}, $False)

    # Drill down
    # Body.ScriptBlock is the part after "Configuration <InstanceName> {"
    # EndBlock is the actual code within that Configuration block
    # Find the first DynamicKeywordStatement that has a word "Node" in it, find all "NamedBlockAst" elements, these are the DSC resource definitions
    $resourceInstances = $Config.Body.ScriptBlock.EndBlock.Statements.Find({$Args[0].GetType().Name -eq 'DynamicKeywordStatementAst' -and $Args[0].CommandElements[0].StringConstantType -eq 'BareWord' -and $Args[0].CommandElements[0].Value -eq 'Node'}, $False).commandElements[2].ScriptBlock.Find({$Args[0].GetType().Name -eq 'NamedBlockAst'}, $False).Statements

    # Get the name of the configuration.
    $configurationName = $Config.InstanceName.Value

    $totalCount = 1
    foreach ($resource in $resourceInstances)
    {
        $currentResourceInfo = @{}

        # CommandElements
        # 0 - Resource Type
        # 1 - Resource Instance Name
        # 2 - Key/Pair Value list of parameters.
        $resourceType         = $resource.CommandElements[0].Value
        $resourceInstanceName = $resource.CommandElements[1].Value

        $percent = ($totalCount / ($resourceInstances.Count) * 100)
        Write-Progress -Status "[$totalCount/$($resourceInstances.Count)] $resourceType - $resourceInstanceName" `
                       -PercentComplete $percent `
                       -Activity "Parsing Resources"
        $currentResourceInfo.Add("ResourceName", $resourceType)
        $currentResourceInfo.Add("ResourceInstanceName", $resourceInstanceName)

        # Get a reference to the current resource.
        $currentResource = $DSCResources | Where-Object -FilterScript {$_.Name -eq $resourceType}

        # Loop through all the key/pair value
        foreach ($keyValuePair in $resource.CommandElements[2].KeyValuePairs)
        {
            $isVariable = $false
            $key        = $keyValuePair.Item1.Value

            if ($null -ne $keyValuePair.Item2.PipelineElements)
            {
                if ($null -eq $keyValuePair.Item2.PipelineElements.Expression.Value)
                {
                    $value = $keyValuePair.Item2.PipelineElements.Expression.ToString()

                    if ($value.StartsWith('$'))
                    {
                        $isVariable = $true
                    }
                }
                else
                {
                    $value = $keyValuePair.Item2.PipelineElements.Expression.Value
                }
            }

            # Retrieve the current property's type based on the resource's schema.
            $currentPropertyInResourceSchema = $currentResource.Properties | Where-Object -FilterScript { $_.Name -eq $key }
            $valueType = $currentPropertyInResourceSchema.PropertyType

            # If the value type is null, then the parameter doesn't exist
            # in the resource's schema and we throw a warning
            $propertyFound = $true
            if ($null -eq $valueType)
            {
                $propertyFound = $false
                Write-Warning "Defined property {$key} was not found in resource {$resourceType}"
            }

            if ($propertyFound)
            {
                # If the current property is not a CIMInstance
                if (-not $valueType.StartsWith('[MSFT_') -and `
                    $valueType -ne '[string]' -and `
                    $valueType -ne '[string[]]')
                {
                    # Try to parse the value based on the retrieved type.
                    $scriptBlock = @"
                                    `$typeStaticMethods = $valueType | gm -static
                                    if (`$typeStaticMethods.Name.Contains('TryParse'))
                                    {
                                        $valueType::TryParse(`$value, [ref]`$value) | Out-Null
                                    }
"@

                    Invoke-Expression -Command $scriptBlock | Out-Null
                }
                elseif ($valueType -eq '[String]' -or $isVariable -or $valueType -eq '[string[]]')
                {
                    $value = $value
                }
                else
                {
                    $value = ConvertFrom-CIMInstanceToHashtable -ChildObject $keyValuePair.Item2
                }
                $currentResourceInfo.Add($key, $value) | Out-Null
            }
        }
        
        $result += $currentResourceInfo
        $totalCount++
    }
    Write-Progress -Completed `
                   -Activity "Parsing Resources"

    if ($IncludeComments)
    {
        $result = Update-DSCResultWithMetadata -Tokens $Tokens `
                                               -ParsedObject $result
    }

    return [Array]$result
}

function ConvertFrom-DSCObject
{
    [CmdletBinding()]
    [OutputType([System.String])]

    Param(
        [parameter(Mandatory = $true)]
        [System.Collections.Hashtable[]]
        $DSCResources,

        [parameter(Mandatory = $false)]
        [System.Int32]
        $ChildLevel = 0
    )

    $results = [System.Text.StringBuilder]::New()
    $ParametersToSkip = @('ResourceInstanceName', 'ResourceName', 'CIMInstance')
    $childSpacer = ""
    for ($i = 0; $i -lt $ChildLevel; $i++)
    {
        $childSpacer += " "
    }
    foreach ($entry in $DSCResources)
    {
        $longuestParameter = [int]($entry.Keys | Measure-Object -Maximum -Property Length).Maximum

        if ($entry.'CIMInstance')
        {
            [void]$results.AppendLine($childSpacer + $entry.CIMInstance)
        }
        else
        {
            [void]$results.AppendLine($childSpacer + $entry.ResourceName + " `"$($entry.ResourceInstanceName)`"")
        }

        [void]$results.AppendLine("$childSpacer{")
        foreach ($property in $entry.Keys)
        {
            if ($property -notin $ParametersToSkip)
            {
                $additionalSpaces = " "
                for ($i = $property.Length; $i -lt $longuestParameter; $i++)
                {
                    $additionalSpaces += " "
                }

                if ($property -eq 'Credential')
                {
                    [void]$results.AppendLine("$childSpacer $property$additionalSpaces= $($entry.$property)")
                }
                else
                {
                    switch($entry.$property.GetType().Name)
                    {
                        "String"
                        {
                            [void]$results.AppendLine("$childSpacer $property$additionalSpaces= `"$($entry.$property.Replace('"', '`"'))`"")
                        }
                        "Int32"
                        {
                            [void]$results.AppendLine("$childSpacer $property$additionalSpaces= $($entry.$property)")
                        }
                        "Boolean"
                        {
                            [void]$results.AppendLine("$childSpacer $property$additionalSpaces= `$$($entry.$property)")
                        }
                        "Object[]"
                        {
                            if ($entry.$property.Length -gt 0)
                            {
                                $objectToTest = $entry.$property
                                if ($null -ne $objectToTest -and $objectToTest.Keys.Length -gt 0)
                                {
                                    if ($objectToTest.'CIMInstance')
                                    {
                                        $subResult = ConvertFrom-DSCObject -DSCResources $entry.$property -ChildLevel ($ChildLevel + 2)
                                        [void]$results.AppendLine("$childSpacer $property$additionalSpaces= @(")
                                        [void]$results.AppendLine("$subResult")
                                        [void]$results.AppendLine("$childSpacer )")
                                    }
                                }
                                else
                                {
                                    switch($entry.$property[0].GetType().Name)
                                    {
                                        "String"
                                        {
                                            [void]$results.Append("$childSpacer $property$additionalSpaces= @(")
                                            $tempArrayContent = ""
                                            foreach ($item in $entry.$property)
                                            {
                                                $tempArrayContent += "`"$($item.Replace('"', '`"'))`","
                                            }
                                            $tempArrayContent = $tempArrayContent.Remove($tempArrayContent.Length-1, 1)
                                            [void]$results.Append($tempArrayContent + ")`r`n")
                                        }
                                        "Int32"
                                        {
                                            [void]$results.Append("$childSpacer $property$additionalSpaces= @(")
                                            $tempArrayContent = ""
                                            foreach ($item in $entry.$property)
                                            {
                                                $tempArrayContent += "$item,"
                                            }
                                            $tempArrayContent = $tempArrayContent.Remove($tempArrayContent.Length-1, 1)
                                            [void]$results.Append($tempArrayContent + ")`r`n")
                                        }
                                    }
                                }
                             }
                        }
                    }
                }
            }
        }
        [void]$results.AppendLine("$childSpacer}")
    }

    return $results.ToString()
}