465 lines
12 KiB
PowerShell
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
|
|
}
|