Files
reflection-config-tool/reflection_config_generator.ps1
T
2026-04-24 07:59:08 +02:00

465 lines
12 KiB
PowerShell

[CmdletBinding()]
param(
[Parameter()]
[string[]]$SourceRoot = @("."),
[Parameter()]
[string[]]$Package = @(),
[Parameter()]
[string]$Output = "reflection-config.json",
[Parameter()]
[switch]$Stdout,
[Parameter()]
[bool]$IncludeDeclared = $true,
[Parameter()]
[bool]$IncludePublic = $false,
[Parameter()]
[bool]$IncludeConstructors = $true,
[Parameter()]
[bool]$IncludeMethods = $true,
[Parameter()]
[bool]$IncludeFields = $true,
[Parameter()]
[switch]$NoSort
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Normalize-PackageFilter {
param(
[Parameter(Mandatory = $true)]
[string]$RawFilter
)
$normalized = $RawFilter.Trim()
if ([string]::IsNullOrWhiteSpace($normalized)) {
return ""
}
if ($normalized.EndsWith(".*", [System.StringComparison]::Ordinal)) {
$normalized = $normalized.Substring(0, $normalized.Length - 2)
}
return $normalized.TrimEnd(".")
}
function Parse-PackageFilters {
param(
[Parameter(Mandatory = $true)]
[string[]]$Values
)
$filters = [System.Collections.Generic.List[string]]::new()
$seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
foreach ($value in $Values) {
foreach ($part in $value.Split(",")) {
$normalized = Normalize-PackageFilter -RawFilter $part
if (-not [string]::IsNullOrWhiteSpace($normalized) -and $seen.Add($normalized)) {
$filters.Add($normalized)
}
}
}
return $filters.ToArray()
}
function Test-PackageMatch {
param(
[Parameter(Mandatory = $true)]
[string]$PackageName,
[Parameter(Mandatory = $true)]
[string[]]$PackageFilters
)
if ($PackageFilters.Count -eq 0) {
return $true
}
foreach ($prefix in $PackageFilters) {
if ($PackageName.Equals($prefix, [System.StringComparison]::Ordinal)) {
return $true
}
if ($PackageName.StartsWith("$prefix.", [System.StringComparison]::Ordinal)) {
return $true
}
}
return $false
}
function Strip-CommentsAndLiterals {
param(
[Parameter(Mandatory = $true)]
[string]$Source
)
$result = [System.Text.StringBuilder]::new($Source.Length)
$state = "normal"
$length = $Source.Length
$i = 0
while ($i -lt $length) {
$char = $Source[$i]
$next = if ($i + 1 -lt $length) { $Source[$i + 1] } else { [char]0 }
if ($state -eq "normal") {
if ($char -eq "/" -and $next -eq "/") {
$state = "line_comment"
[void]$result.Append(" ")
$i += 2
continue
}
if ($char -eq "/" -and $next -eq "*") {
$state = "block_comment"
[void]$result.Append(" ")
$i += 2
continue
}
if ($char -eq '"') {
$state = "string"
[void]$result.Append(" ")
$i += 1
continue
}
if ($char -eq "'") {
$state = "char"
[void]$result.Append(" ")
$i += 1
continue
}
[void]$result.Append($char)
$i += 1
continue
}
if ($state -eq "line_comment") {
if ($char -eq "`n") {
$state = "normal"
[void]$result.Append("`n")
}
else {
[void]$result.Append(" ")
}
$i += 1
continue
}
if ($state -eq "block_comment") {
if ($char -eq "*" -and $next -eq "/") {
$state = "normal"
[void]$result.Append(" ")
$i += 2
}
else {
if ($char -eq "`n") {
[void]$result.Append("`n")
}
else {
[void]$result.Append(" ")
}
$i += 1
}
continue
}
if ($state -eq "string") {
if ($char -eq "\" -and $i + 1 -lt $length) {
[void]$result.Append(" ")
$i += 2
continue
}
if ($char -eq '"') {
$state = "normal"
[void]$result.Append(" ")
}
else {
if ($char -eq "`n") {
[void]$result.Append("`n")
}
else {
[void]$result.Append(" ")
}
}
$i += 1
continue
}
if ($state -eq "char") {
if ($char -eq "\" -and $i + 1 -lt $length) {
[void]$result.Append(" ")
$i += 2
continue
}
if ($char -eq "'") {
$state = "normal"
[void]$result.Append(" ")
}
else {
if ($char -eq "`n") {
[void]$result.Append("`n")
}
else {
[void]$result.Append(" ")
}
}
$i += 1
continue
}
}
return $result.ToString()
}
function Get-PackageName {
param(
[Parameter(Mandatory = $true)]
[string]$CleanedSource
)
$pattern = "^\s*package\s+([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\s*;"
$match = [regex]::Match(
$CleanedSource,
$pattern,
[System.Text.RegularExpressions.RegexOptions]::Multiline
)
if (-not $match.Success) {
return $null
}
return $match.Groups[1].Value
}
function Get-TopLevelTypes {
param(
[Parameter(Mandatory = $true)]
[string]$CleanedSource
)
$keywords = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
[void]$keywords.Add("class")
[void]$keywords.Add("interface")
[void]$keywords.Add("enum")
[void]$keywords.Add("record")
$matches = [regex]::Matches($CleanedSource, "[A-Za-z_][A-Za-z0-9_]*|[{}]")
$depth = 0
$types = [System.Collections.Generic.List[string]]::new()
$seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
$i = 0
while ($i -lt $matches.Count) {
$token = $matches[$i].Value
if ($token -eq "{") {
$depth += 1
$i += 1
continue
}
if ($token -eq "}") {
if ($depth -gt 0) {
$depth -= 1
}
$i += 1
continue
}
if ($depth -eq 0 -and $keywords.Contains($token)) {
if ($i + 1 -lt $matches.Count) {
$candidate = $matches[$i + 1].Value
if ([regex]::IsMatch($candidate, "^[A-Za-z_][A-Za-z0-9_]*$")) {
if ($seen.Add($candidate)) {
$types.Add($candidate)
}
$i += 2
continue
}
}
}
$i += 1
}
return $types.ToArray()
}
function Get-ClassNamesFromFile {
param(
[Parameter(Mandatory = $true)]
[string]$JavaFile,
[Parameter(Mandatory = $true)]
[string[]]$PackageFilters
)
$source = Get-Content -LiteralPath $JavaFile -Raw -Encoding UTF8
$cleaned = Strip-CommentsAndLiterals -Source $source
$packageName = Get-PackageName -CleanedSource $cleaned
if ([string]::IsNullOrWhiteSpace($packageName)) {
return @()
}
if (-not (Test-PackageMatch -PackageName $packageName -PackageFilters $PackageFilters)) {
return @()
}
$types = Get-TopLevelTypes -CleanedSource $cleaned
$result = [System.Collections.Generic.List[string]]::new()
foreach ($typeName in $types) {
$result.Add("$packageName.$typeName")
}
return $result.ToArray()
}
function New-ReflectionEntry {
param(
[Parameter(Mandatory = $true)]
[string]$ClassName,
[Parameter(Mandatory = $true)]
[bool]$IncludeDeclared,
[Parameter(Mandatory = $true)]
[bool]$IncludePublic,
[Parameter(Mandatory = $true)]
[bool]$IncludeConstructors,
[Parameter(Mandatory = $true)]
[bool]$IncludeMethods,
[Parameter(Mandatory = $true)]
[bool]$IncludeFields
)
$entry = [ordered]@{
name = $ClassName
}
if ($IncludeDeclared) {
if ($IncludeConstructors) {
$entry["allDeclaredConstructors"] = $true
}
if ($IncludeMethods) {
$entry["allDeclaredMethods"] = $true
}
if ($IncludeFields) {
$entry["allDeclaredFields"] = $true
}
}
if ($IncludePublic) {
if ($IncludeConstructors) {
$entry["allPublicConstructors"] = $true
}
if ($IncludeMethods) {
$entry["allPublicMethods"] = $true
}
if ($IncludeFields) {
$entry["allPublicFields"] = $true
}
}
return [pscustomobject]$entry
}
try {
$resolvedRoots = [System.Collections.Generic.List[string]]::new()
foreach ($root in $SourceRoot) {
if (-not (Test-Path -LiteralPath $root -PathType Container)) {
throw "Missing source root: $root"
}
$resolvedPath = (Resolve-Path -LiteralPath $root).Path
$resolvedRoots.Add($resolvedPath)
}
$packageFilters = Parse-PackageFilters -Values $Package
$javaFiles = [System.Collections.Generic.List[string]]::new()
foreach ($root in $resolvedRoots) {
$files = Get-ChildItem -Path $root -Recurse -Filter "*.java" -File
foreach ($file in $files) {
$javaFiles.Add($file.FullName)
}
}
$classNames = [System.Collections.Generic.List[string]]::new()
$seenClassNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
foreach ($javaFile in $javaFiles) {
try {
$classes = Get-ClassNamesFromFile -JavaFile $javaFile -PackageFilters $packageFilters
foreach ($className in $classes) {
if ($seenClassNames.Add($className)) {
$classNames.Add($className)
}
}
}
catch {
Write-Warning "Skipping unreadable file $javaFile : $($_.Exception.Message)"
}
}
$finalClassNames = $classNames.ToArray()
if (-not $NoSort) {
$finalClassNames = $finalClassNames | Sort-Object
}
$reflectionEntries = [System.Collections.Generic.List[object]]::new()
foreach ($className in $finalClassNames) {
$reflectionEntries.Add(
(New-ReflectionEntry `
-ClassName $className `
-IncludeDeclared $IncludeDeclared `
-IncludePublic $IncludePublic `
-IncludeConstructors $IncludeConstructors `
-IncludeMethods $IncludeMethods `
-IncludeFields $IncludeFields)
)
}
$outputJson = $reflectionEntries | ConvertTo-Json -Depth 5
if ($Stdout) {
Write-Output $outputJson
}
else {
$outputPath = [System.IO.Path]::GetFullPath($Output)
$outputDir = [System.IO.Path]::GetDirectoryName($outputPath)
if (-not [string]::IsNullOrWhiteSpace($outputDir) -and -not (Test-Path -LiteralPath $outputDir)) {
[void](New-Item -ItemType Directory -Path $outputDir -Force)
}
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
[System.IO.File]::WriteAllText($outputPath, $outputJson + [Environment]::NewLine, $utf8NoBom)
Write-Output "Wrote $($reflectionEntries.Count) entries to $outputPath"
}
}
catch {
Write-Error $_.Exception.Message
exit 1
}