[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 }