解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
大石头 authored at 2018-05-15 21:21:05
7.11 KiB
X
# 审计多语言元数据覆盖:扫描源码中的属性/程序集元数据键,报告各语言缺键与覆盖矩阵
# 同时算出「非源码来源键」(词表里存在但源码找不到,如语言名),它们是 metadata-keys.json 的内容。
#
# 用法:
#   pwsh -File Tools/Scripts/collect-metadata-keys.ps1                     # 只审计,不写清单
#   pwsh -File Tools/Scripts/collect-metadata-keys.ps1 -Update             # 重写 metadata-keys.json
#   pwsh -File Tools/Scripts/collect-metadata-keys.ps1 -OutFile coverage.csv
param(
    [String]$LocaleDir = "NewLife.Core\Locales",
    [String[]]$SourceDir = @("NewLife.Core", "NewLife.Core.SourceGenerator"),
    [String]$Output = "metadata-keys.json",
    [Switch]$Update,
    [String]$OutFile
)

$ErrorActionPreference = "Stop"

$root = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$localeDir = if ([IO.Path]::IsPathRooted($LocaleDir)) { $LocaleDir } else { Join-Path $root $LocaleDir }
if (!(Test-Path $localeDir)) {
    Write-Host "未找到语言目录:$localeDir" -ForegroundColor Red
    exit 1
}
$outputPath = if ([IO.Path]::IsPathRooted($Output)) { $Output } else { Join-Path $localeDir $Output }

$pattern = '^\s*"((?:[^"\\]|\\.)*)"\s*:\s*"((?:[^"\\]|\\.)*)"\s*$'
function Unescape-Yaml([string]$text) {
    return [regex]::Replace($text, '\\(u[0-9a-fA-F]{4}|.)', {
        param($m)
        $v = $m.Groups[1].Value
        if ($v.Length -eq 5) { return [string][char][Convert]::ToInt32($v.Substring(1), 16) }
        switch -CaseSensitive ($v) {
            'n' { return "`n" }
            'r' { return "`r" }
            't' { return "`t" }
            '\\' { return '\' }
            '"' { return '"' }
            default { return $m.Value }
        }
    })
}

# 扫描源码:特性参数与 csproj 的程序集元数据
$sourceDirs = @($SourceDir | ForEach-Object {
    if ([IO.Path]::IsPathRooted($_)) { $_ } else { Join-Path $root $_ }
} | Where-Object { Test-Path $_ })

$attributeKeys = [Collections.Generic.HashSet[String]]::new([StringComparer]::Ordinal)
foreach ($file in ($sourceDirs | ForEach-Object { Get-ChildItem $_ -Recurse -Include *.cs, *.csproj } |
    Where-Object { $_.FullName -notmatch '\\(obj|bin|Locales)\\' })) {
    if ($file.Extension -eq ".csproj") {
        $text = [IO.File]::ReadAllText($file.FullName)
        # 构建期分析器(Roslyn 组件)不对用户展示,其程序集描述不参与本地化
        if ($text.Contains("<IsRoslynComponent>true</IsRoslynComponent>")) { continue }
        foreach ($m in [regex]::Matches($text, '<(?:AssemblyTitle|Description)>([^<]+)</(?:AssemblyTitle|Description)>')) {
            $value = [System.Net.WebUtility]::HtmlDecode($m.Groups[1].Value.Trim())
            if ($value.Length -gt 0) { [void]$attributeKeys.Add($value) }
        }
        continue
    }

    # 逐行扫描并跳过注释行,避免把注释掉的特性当成源文
    $inBlock = $false
    foreach ($line in Get-Content -LiteralPath $file.FullName -Encoding UTF8) {
        $trimmed = $line.Trim()
        if ($inBlock) {
            if ($trimmed.Contains("*/")) { $inBlock = $false }
            continue
        }
        if ($trimmed.StartsWith("/*")) { $inBlock = !$trimmed.Contains("*/"); continue }
        if ($trimmed.StartsWith("//")) { continue }

        foreach ($m in [regex]::Matches($trimmed, '\[(?:DisplayName|Description)\("((?:[^"\\]|\\.)*)"')) {
            $value = $m.Groups[1].Value.Replace('\"', '"').Replace('\\', '\')
            if ($value.Length -gt 0) { [void]$attributeKeys.Add($value) }
        }
    }
}

# 读取各语言词条
$tables = [ordered]@{}
foreach ($file in Get-ChildItem $localeDir -Filter *.yml | Sort-Object Name) {
    $entries = [Collections.Generic.HashSet[String]]::new([StringComparer]::Ordinal)
    foreach ($line in Get-Content -LiteralPath $file.FullName -Encoding UTF8) {
        $m = [regex]::Match($line, $pattern)
        if ($m.Success) { [void]$entries.Add((Unescape-Yaml $m.Groups[1].Value)) }
    }
    $tables[$file.BaseName] = $entries
}

# 基准语言(zh-Hans.yml)必须覆盖全部元数据键
$baselineName = "zh-Hans"
$baseline = $tables[$baselineName]
if ($null -eq $baseline) { $baseline = $tables[(Get-ChildItem $localeDir -Filter *.yml | Select-Object -First 1).BaseName] }

$missingInBaseline = @($attributeKeys | Where-Object { !$baseline.Contains($_) } | Sort-Object)

# 词表里存在、但源码中找不到的键:这些才是需要登记到 metadata-keys.json 的「非源码来源键」
$sourceText = ($sourceDirs | ForEach-Object { Get-ChildItem $_ -Recurse -Include *.cs, *.csproj } |
    Where-Object { $_.FullName -notmatch '\\(obj|bin|Locales)\\' } |
    ForEach-Object { [IO.File]::ReadAllText($_.FullName) }) -join "`n"
$registryKeys = [Collections.Generic.SortedSet[String]]::new([StringComparer]::Ordinal)
foreach ($key in $baseline) {
    $escaped = $key.Replace('\', '\\').Replace('"', '\"').Replace("`t", '\t').Replace("`r", '\r').Replace("`n", '\n')
    if (!$sourceText.Contains($key) -and !$sourceText.Contains($escaped)) { [void]$registryKeys.Add($key) }
}

if ($Update) {
    $json = [ordered]@{
        "comment" = "非源码来源键(语言名、外部元数据等),豁免孤儿键判定;词表仍要求全语言齐全。由 collect-metadata-keys.ps1 -Update 生成。"
        "updated" = (Get-Date -Format "yyyy-MM-dd")
        "keys"    = @($registryKeys)
    }
    [IO.File]::WriteAllText($outputPath, ($json | ConvertTo-Json -Depth 3), [Text.UTF8Encoding]::new($false))
    Write-Host "已写入非源码来源键清单:$($registryKeys.Count) 条 → $outputPath"
}
else {
    Write-Host "非源码来源键候选:$($registryKeys.Count) 条(加 -Update 写入清单)"
}

if ($OutFile) {
    $rows = @()
    foreach ($key in $attributeKeys) {
        $row = [ordered]@{ Key = $key }
        foreach ($culture in $tables.Keys) { $row[$culture] = if ($tables[$culture].Contains($key)) { "Y" } else { "" } }
        $rows += [pscustomobject]$row
    }
    $rows | Export-Csv -LiteralPath $OutFile -NoTypeInformation -Encoding UTF8
    Write-Host "已导出覆盖矩阵:$OutFile"
}

Write-Host "源码元数据键:$($attributeKeys.Count) 条"
if ($missingInBaseline.Count -gt 0) {
    Write-Host "基准语言缺少以下元数据键,请补入 $baselineName.yml:" -ForegroundColor Yellow
    foreach ($key in $missingInBaseline) { Write-Host "  $key" -ForegroundColor Yellow }
}

# 全语言覆盖检查:元数据键必须出现在每一种语言
$incomplete = @{}
foreach ($key in $attributeKeys) {
    if (!$baseline.Contains($key)) { continue }
    foreach ($culture in $tables.Keys) {
        if ($culture -eq "en") { continue }
        if (!$tables[$culture].Contains($key)) {
            if (!$incomplete.ContainsKey($key)) { $incomplete[$key] = @() }
            $incomplete[$key] += $culture
        }
    }
}
if ($incomplete.Count -gt 0) {
    Write-Host "以下元数据键未覆盖全部语言:" -ForegroundColor Yellow
    foreach ($key in ($incomplete.Keys | Sort-Object)) { Write-Host ("  {0} → 缺 {1}" -f $key, ($incomplete[$key] -join ',')) -ForegroundColor Yellow }
}
else {
    Write-Host "元数据键全语言覆盖:通过($($attributeKeys.Count) 条)"
}