task/Test/TestAttributeSyntax.ps1

BuildTask TestAttributeSyntax -Stage Test -Order 1 -Definition {
    # Attempt to test whether or not attributes used within a script contain errors.
    #
    # If an attribute does not appear to exist it is compared with a list of common attributes from the System.Management.Automation namespace and any classes declared within the module.
    #
    # If the non-existent attribute has a Levenshtein distance less than 3 from a known attribute it will be flagged as a typo and the build will fail.
    #
    # Otherwise the author is assumed to have implemented and used a new attribute which is declared elsewhere.

    $script = {
        param (
            $buildInfo
        )

        $path = Join-Path $buildInfo.Path.Source.Module 'test*'

        if (Test-Path (Join-Path $path 'stub')) {
            Get-ChildItem (Join-Path $path 'stub') -Filter *.psm1 -Recurse -Depth 1 | ForEach-Object {
                Import-Module $_.FullName -Global -WarningAction SilentlyContinue
            }
        }

        Import-Module $buildInfo.Path.Build.Manifest

        $commonAttributes = [PowerShell].Assembly.GetTypes() |
            Where-Object { $_.Name -match 'Attribute$' -and $_.IsPublic } |
            ForEach-Object {
                $_.Name
                $_.Name -replace 'Attribute$'
            }

        $hasSyntaxErrors = $false
        $tokens = $null
        [System.Management.Automation.Language.ParseError[]]$parseErrors = @()
        $ast = [System.Management.Automation.Language.Parser]::ParseFile(
            $buildInfo.Path.Build.RootModule,
            [Ref]$tokens,
            [Ref]$parseErrors
        )
        $moduleClasses = $ast.FindAll(
            {
                param ( $childAst )

                $childAst -is [System.Management.Automation.Language.TypeDefinitionAst] -and
                $childAst.IsClass
            },
            $true
        ) | Group-Object Name -AsHashTable -AsString

        $attributes = $ast.FindAll(
            {
                param ( $childAst )

                $childAst -is [System.Management.Automation.Language.AttributeAst]
            },
            $true
        )
        foreach ($attribute in $attributes) {
            if ($moduleClasses -and $moduleClasses.Contains($attribute.TypeName.FullName)) {
                continue
            } elseif (($type = $attribute.TypeName.FullName -as [Type]) -or ($type = ('{0}Attribute' -f $attribute.TypeName.FullName) -as [Type])) {
                $propertyNames = $type.GetProperties().Name

                if ($attribute.NamedArguments.Count -gt 0) {
                    foreach ($argument in $attribute.NamedArguments) {
                        if ($argument.ArgumentName -notin $propertyNames) {
                            Write-Warning ('Invalid property name in attribute declaration: {0} at line {1}' -f @(
                                $argument.ArgumentName
                                $argument.Extent.StartLineNumber
                            ))

                            $hasSyntaxErrors = $true
                        }
                    }
                }
            } else {
                $params = @{
                    ReferenceString  = $attribute.TypeName.Name
                }
                $closestMatch = $commonAttributes |
                    Get-LevenshteinDistance @params |
                    Where-Object Distance -lt 3 |
                    Select-Object -First 1

                $message = 'Unknown attribute declared: {0} at line {1}.'
                if ($closestMatch) {
                    $message = '{0} Suggested name: {1}' -f @(
                        $message
                        $closestMatch.DifferenceString
                    )
                    $hasSyntaxErrors = $true
                }

                Write-Warning ($message -f @(
                    $attribute.TypeName.FullName
                    $attribute.Extent.StartLineNumber
                ))

            }
        }

        return $hasSyntaxErrors
    }

    if ($buildInfo.BuildSystem -eq 'Desktop') {
        $hasSyntaxErrors = Start-Job -ArgumentList $buildInfo -ScriptBlock $script | Receive-Job -Wait
    } else {
        $hasSyntaxErrors = & $script -BuildInfo $buildInfo
    }

    if ($hasSyntaxErrors) {
        throw 'TestAttributeSyntax failed'
    }
}