322 lines
14 KiB
PowerShell
322 lines
14 KiB
PowerShell
param(
|
|
[string]$RepoRoot = "",
|
|
[string]$ConfigPath = "",
|
|
[string]$ReportPath = ""
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
function Resolve-AbsolutePath {
|
|
param(
|
|
[string]$Path,
|
|
[string]$BasePath
|
|
)
|
|
|
|
if ([System.IO.Path]::IsPathRooted($Path)) {
|
|
return [System.IO.Path]::GetFullPath($Path)
|
|
}
|
|
return [System.IO.Path]::GetFullPath((Join-Path $BasePath $Path))
|
|
}
|
|
|
|
function Get-RelativePath {
|
|
param(
|
|
[string]$Root,
|
|
[string]$Path
|
|
)
|
|
|
|
$rootPrefix = [System.IO.Path]::GetFullPath($Root).TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar
|
|
$fullPath = [System.IO.Path]::GetFullPath($Path)
|
|
if (-not $fullPath.StartsWith($rootPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "Path is outside repository root: $fullPath"
|
|
}
|
|
return $fullPath.Substring($rootPrefix.Length).Replace('\', '/')
|
|
}
|
|
|
|
function Get-TextFiles {
|
|
param(
|
|
[string]$Root,
|
|
[object[]]$Paths
|
|
)
|
|
|
|
$allowedExtensions = @(".h", ".hpp", ".cpp", ".cxx", ".md", ".txt", ".json", ".cmake", ".ps1")
|
|
$files = New-Object System.Collections.Generic.List[System.IO.FileInfo]
|
|
foreach ($relativePath in $Paths) {
|
|
$absolutePath = Resolve-AbsolutePath -Path ([string]$relativePath) -BasePath $Root
|
|
if (-not (Test-Path -LiteralPath $absolutePath)) {
|
|
continue
|
|
}
|
|
$item = Get-Item -LiteralPath $absolutePath
|
|
if (-not $item.PSIsContainer) {
|
|
$files.Add($item)
|
|
continue
|
|
}
|
|
foreach ($file in Get-ChildItem -LiteralPath $absolutePath -Recurse -File) {
|
|
if ($allowedExtensions -contains $file.Extension.ToLowerInvariant()) {
|
|
$files.Add($file)
|
|
}
|
|
}
|
|
}
|
|
return @($files | Sort-Object FullName -Unique)
|
|
}
|
|
|
|
function Invoke-GitDiffCheck {
|
|
param([string]$Root)
|
|
|
|
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
|
|
$startInfo.FileName = "git"
|
|
$escapedRoot = $Root.Replace('"', '\"')
|
|
$startInfo.Arguments = "-C `"$escapedRoot`" diff --check"
|
|
$startInfo.UseShellExecute = $false
|
|
$startInfo.CreateNoWindow = $true
|
|
$startInfo.RedirectStandardOutput = $true
|
|
$startInfo.RedirectStandardError = $true
|
|
|
|
$process = New-Object System.Diagnostics.Process
|
|
$process.StartInfo = $startInfo
|
|
if (-not $process.Start()) {
|
|
throw "Unable to start git for diff validation."
|
|
}
|
|
$standardOutput = $process.StandardOutput.ReadToEnd()
|
|
$standardError = $process.StandardError.ReadToEnd()
|
|
$process.WaitForExit()
|
|
|
|
return [pscustomobject]@{
|
|
ExitCode = $process.ExitCode
|
|
Output = ($standardOutput + $standardError).Trim()
|
|
}
|
|
}
|
|
|
|
$defaultRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..\..\.."))
|
|
$resolvedRoot = if ([string]::IsNullOrWhiteSpace($RepoRoot)) {
|
|
$defaultRoot
|
|
} else {
|
|
Resolve-AbsolutePath -Path $RepoRoot -BasePath (Get-Location)
|
|
}
|
|
$resolvedConfig = if ([string]::IsNullOrWhiteSpace($ConfigPath)) {
|
|
Join-Path $resolvedRoot "internal/qa/config/static-checks.json"
|
|
} else {
|
|
Resolve-AbsolutePath -Path $ConfigPath -BasePath $resolvedRoot
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $resolvedConfig -PathType Leaf)) {
|
|
throw "Static-check configuration was not found: $resolvedConfig"
|
|
}
|
|
|
|
$config = Get-Content -LiteralPath $resolvedConfig -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
if ($config.schemaVersion -ne 1) {
|
|
throw "Unsupported static-check configuration version: $($config.schemaVersion)"
|
|
}
|
|
|
|
$results = New-Object System.Collections.Generic.List[object]
|
|
function Add-Result {
|
|
param(
|
|
[string]$Name,
|
|
[bool]$Passed,
|
|
[string]$Details
|
|
)
|
|
$results.Add([pscustomobject]@{
|
|
Name = $Name
|
|
Passed = $Passed
|
|
Details = $Details
|
|
})
|
|
}
|
|
|
|
$cmakePath = Resolve-AbsolutePath -Path ([string]$config.productionCmakeFile) -BasePath $resolvedRoot
|
|
$cmakeText = Get-Content -LiteralPath $cmakePath -Raw -Encoding UTF8
|
|
$productionFiles = New-Object System.Collections.Generic.List[System.IO.FileInfo]
|
|
foreach ($sourceRoot in $config.productionSourceRoots) {
|
|
$sourceRootPath = Resolve-AbsolutePath -Path ([string]$sourceRoot) -BasePath $resolvedRoot
|
|
foreach ($file in Get-ChildItem -LiteralPath $sourceRootPath -Recurse -File) {
|
|
if ($config.productionExtensions -contains $file.Extension.ToLowerInvariant()) {
|
|
$productionFiles.Add($file)
|
|
}
|
|
}
|
|
}
|
|
|
|
$missingFromCmake = New-Object System.Collections.Generic.List[string]
|
|
foreach ($file in $productionFiles) {
|
|
$relativePath = Get-RelativePath -Root $resolvedRoot -Path $file.FullName
|
|
if (-not $cmakeText.Contains($relativePath)) {
|
|
$missingFromCmake.Add($relativePath)
|
|
}
|
|
}
|
|
Add-Result -Name "Production sources are registered in CMake" `
|
|
-Passed ($missingFromCmake.Count -eq 0) `
|
|
-Details ($(if ($missingFromCmake.Count -eq 0) { "$($productionFiles.Count) source/header files registered" } else { "Missing: " + ($missingFromCmake -join ", ") }))
|
|
|
|
$missingCmakeReferences = New-Object System.Collections.Generic.List[string]
|
|
$cmakeReferencePattern = 'src[\\/][A-Za-z0-9_.\\/-]+\.(?:h|cpp)'
|
|
foreach ($match in [regex]::Matches($cmakeText, $cmakeReferencePattern)) {
|
|
$relativePath = $match.Value.Replace('\', '/')
|
|
if (-not (Test-Path -LiteralPath (Join-Path $resolvedRoot $relativePath) -PathType Leaf)) {
|
|
$missingCmakeReferences.Add($relativePath)
|
|
}
|
|
}
|
|
Add-Result -Name "CMake does not reference missing production sources" `
|
|
-Passed ($missingCmakeReferences.Count -eq 0) `
|
|
-Details ($(if ($missingCmakeReferences.Count -eq 0) { "No missing source references" } else { "Missing: " + (($missingCmakeReferences | Sort-Object -Unique) -join ", ") }))
|
|
|
|
$qaCmakePath = Resolve-AbsolutePath -Path ([string]$config.qaCmakeFile) -BasePath $resolvedRoot
|
|
$qaRoot = Split-Path $qaCmakePath -Parent
|
|
$qaCmakeText = Get-Content -LiteralPath $qaCmakePath -Raw -Encoding UTF8
|
|
$missingQaTests = New-Object System.Collections.Generic.List[string]
|
|
$qaTestCount = 0
|
|
foreach ($testRoot in $config.qaTestRoots) {
|
|
$testRootPath = Resolve-AbsolutePath -Path ([string]$testRoot) -BasePath $resolvedRoot
|
|
foreach ($testFile in Get-ChildItem -LiteralPath $testRootPath -Recurse -Filter "*.cpp" -File) {
|
|
++$qaTestCount
|
|
$relativeTestPath = Get-RelativePath -Root $qaRoot -Path $testFile.FullName
|
|
if (-not $qaCmakeText.Contains($relativeTestPath)) {
|
|
$missingQaTests.Add($relativeTestPath)
|
|
}
|
|
}
|
|
}
|
|
Add-Result -Name "Internal QA test sources are registered in the QA CMake project" `
|
|
-Passed ($missingQaTests.Count -eq 0) `
|
|
-Details ($(if ($missingQaTests.Count -eq 0) { "$qaTestCount test translation units registered" } else { "Missing: " + ($missingQaTests -join ", ") }))
|
|
|
|
$activeFiles = Get-TextFiles -Root $resolvedRoot -Paths $config.activeScanPaths
|
|
foreach ($rule in $config.forbiddenReferences) {
|
|
$matches = New-Object System.Collections.Generic.List[string]
|
|
foreach ($file in $activeFiles) {
|
|
$content = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8
|
|
if ($content -match $rule.pattern) {
|
|
$matches.Add((Get-RelativePath -Root $resolvedRoot -Path $file.FullName))
|
|
}
|
|
}
|
|
Add-Result -Name "No $($rule.name)" `
|
|
-Passed ($matches.Count -eq 0) `
|
|
-Details ($(if ($matches.Count -eq 0) { "No active references" } else { "Found in: " + (($matches | Sort-Object -Unique) -join ", ") }))
|
|
}
|
|
|
|
if ($config.forbidQObjectMacro) {
|
|
$qObjectFiles = New-Object System.Collections.Generic.List[string]
|
|
foreach ($file in $productionFiles) {
|
|
$content = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8
|
|
if ($content -match '\bQ_OBJECT\b') {
|
|
$qObjectFiles.Add((Get-RelativePath -Root $resolvedRoot -Path $file.FullName))
|
|
}
|
|
}
|
|
Add-Result -Name "Production code remains compatible with CMAKE_AUTOMOC OFF" `
|
|
-Passed ($qObjectFiles.Count -eq 0) `
|
|
-Details ($(if ($qObjectFiles.Count -eq 0) { "No Q_OBJECT macro found" } else { "Found in: " + ($qObjectFiles -join ", ") }))
|
|
}
|
|
|
|
$missingRequiredPaths = @($config.requiredPaths | Where-Object {
|
|
-not (Test-Path -LiteralPath (Resolve-AbsolutePath -Path ([string]$_) -BasePath $resolvedRoot))
|
|
})
|
|
Add-Result -Name "Required release inputs exist" `
|
|
-Passed ($missingRequiredPaths.Count -eq 0) `
|
|
-Details ($(if ($missingRequiredPaths.Count -eq 0) { "$($config.requiredPaths.Count) required paths found" } else { "Missing: " + ($missingRequiredPaths -join ", ") }))
|
|
|
|
$characterManifestPath = Join-Path $resolvedRoot "resources/characters/shiroko/character.json"
|
|
$characterProblems = New-Object System.Collections.Generic.List[string]
|
|
try {
|
|
$character = Get-Content -LiteralPath $characterManifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
if ([string]::IsNullOrWhiteSpace([string]$character.defaultState)) {
|
|
$characterProblems.Add("defaultState is empty")
|
|
}
|
|
foreach ($stateProperty in $character.states.PSObject.Properties) {
|
|
$statePath = Join-Path (Split-Path $characterManifestPath -Parent) ([string]$stateProperty.Value.path)
|
|
if (-not (Test-Path -LiteralPath $statePath -PathType Container)) {
|
|
$characterProblems.Add("state directory missing: $($stateProperty.Name)")
|
|
continue
|
|
}
|
|
if (@(Get-ChildItem -LiteralPath $statePath -Filter "*.png" -File).Count -eq 0) {
|
|
$characterProblems.Add("state has no PNG frames: $($stateProperty.Name)")
|
|
}
|
|
}
|
|
} catch {
|
|
$characterProblems.Add("manifest parse failed: $($_.Exception.Message)")
|
|
}
|
|
Add-Result -Name "Default character manifest and frames are complete" `
|
|
-Passed ($characterProblems.Count -eq 0) `
|
|
-Details ($(if ($characterProblems.Count -eq 0) { "All declared states contain PNG frames" } else { $characterProblems -join "; " }))
|
|
|
|
$exportScriptPath = Join-Path $resolvedRoot "tools/prepare_github_export.ps1"
|
|
$exportScriptText = Get-Content -LiteralPath $exportScriptPath -Raw -Encoding UTF8
|
|
$missingExportExclusions = @($config.publicExportExcludedRoots | Where-Object {
|
|
$quotedName = '"' + [regex]::Escape([string]$_) + '"'
|
|
$exportScriptText -notmatch $quotedName
|
|
})
|
|
Add-Result -Name "Public GitHub export excludes internal development roots" `
|
|
-Passed ($missingExportExclusions.Count -eq 0) `
|
|
-Details ($(if ($missingExportExclusions.Count -eq 0) { "Excluded: " + ($config.publicExportExcludedRoots -join ", ") } else { "Not excluded: " + ($missingExportExclusions -join ", ") }))
|
|
|
|
$releaseIsolationMatches = New-Object System.Collections.Generic.List[string]
|
|
foreach ($relativeFile in $config.releaseIsolationFiles) {
|
|
$absoluteFile = Resolve-AbsolutePath -Path ([string]$relativeFile) -BasePath $resolvedRoot
|
|
$content = Get-Content -LiteralPath $absoluteFile -Raw -Encoding UTF8
|
|
foreach ($pattern in $config.releaseForbiddenPatterns) {
|
|
if ($content -match $pattern) {
|
|
$releaseIsolationMatches.Add("$relativeFile -> $pattern")
|
|
}
|
|
}
|
|
}
|
|
Add-Result -Name "Production build and packaging do not depend on internal QA" `
|
|
-Passed ($releaseIsolationMatches.Count -eq 0) `
|
|
-Details ($(if ($releaseIsolationMatches.Count -eq 0) { "No internal QA dependency" } else { $releaseIsolationMatches -join "; " }))
|
|
|
|
$infrastructureProblems = New-Object System.Collections.Generic.List[string]
|
|
foreach ($scriptFile in Get-ChildItem -LiteralPath (Join-Path $resolvedRoot "internal/qa/scripts") -Filter "*.ps1" -File) {
|
|
$tokens = $null
|
|
$parseErrors = $null
|
|
[System.Management.Automation.Language.Parser]::ParseFile(
|
|
$scriptFile.FullName,
|
|
[ref]$tokens,
|
|
[ref]$parseErrors) | Out-Null
|
|
foreach ($parseError in $parseErrors) {
|
|
$infrastructureProblems.Add("$($scriptFile.Name):$($parseError.Extent.StartLineNumber): $($parseError.Message)")
|
|
}
|
|
}
|
|
foreach ($jsonFile in Get-ChildItem -LiteralPath (Join-Path $resolvedRoot "internal/qa/config") -Filter "*.json" -File) {
|
|
try {
|
|
Get-Content -LiteralPath $jsonFile.FullName -Raw -Encoding UTF8 | ConvertFrom-Json | Out-Null
|
|
} catch {
|
|
$infrastructureProblems.Add("$($jsonFile.Name): $($_.Exception.Message)")
|
|
}
|
|
}
|
|
Add-Result -Name "Internal QA scripts and configuration parse successfully" `
|
|
-Passed ($infrastructureProblems.Count -eq 0) `
|
|
-Details ($(if ($infrastructureProblems.Count -eq 0) { "All PowerShell and JSON files parsed" } else { $infrastructureProblems -join "; " }))
|
|
|
|
$gitResult = Invoke-GitDiffCheck -Root $resolvedRoot
|
|
Add-Result -Name "Git diff whitespace check" `
|
|
-Passed ($gitResult.ExitCode -eq 0) `
|
|
-Details ($(if ($gitResult.ExitCode -eq 0) { "git diff --check passed" } else { $gitResult.Output }))
|
|
|
|
$passedCount = @($results | Where-Object Passed).Count
|
|
$failedResults = @($results | Where-Object { -not $_.Passed })
|
|
foreach ($result in $results) {
|
|
$prefix = if ($result.Passed) { "PASS" } else { "FAIL" }
|
|
Write-Host "[$prefix] $($result.Name) - $($result.Details)"
|
|
}
|
|
Write-Host "Static checks: $passedCount passed, $($failedResults.Count) failed."
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($ReportPath)) {
|
|
$resolvedReportPath = Resolve-AbsolutePath -Path $ReportPath -BasePath $resolvedRoot
|
|
$reportDirectory = Split-Path $resolvedReportPath -Parent
|
|
if (-not (Test-Path -LiteralPath $reportDirectory)) {
|
|
New-Item -ItemType Directory -Path $reportDirectory -Force | Out-Null
|
|
}
|
|
$lines = New-Object System.Collections.Generic.List[string]
|
|
$lines.Add("# Static Check Report")
|
|
$lines.Add("")
|
|
$lines.Add("Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz')")
|
|
$lines.Add("")
|
|
$lines.Add("| Check | Result | Details |")
|
|
$lines.Add("| --- | --- | --- |")
|
|
foreach ($result in $results) {
|
|
$status = if ($result.Passed) { "PASS" } else { "FAIL" }
|
|
$details = ([string]$result.Details).Replace("|", "\|").Replace("`r", " ").Replace("`n", " ")
|
|
$lines.Add("| $($result.Name) | $status | $details |")
|
|
}
|
|
Set-Content -LiteralPath $resolvedReportPath -Value $lines -Encoding UTF8
|
|
Write-Host "Report: $resolvedReportPath"
|
|
}
|
|
|
|
if ($failedResults.Count -gt 0) {
|
|
exit 1
|
|
}
|