From b7b4dbeaa0a9cc0e496309ce8ad1da6fe3bf41a0 Mon Sep 17 00:00:00 2001 From: admrene Date: Fri, 24 Apr 2026 07:59:08 +0200 Subject: [PATCH] Init Commit --- README.md | 64 +++ ...eflection_config_generator.cpython-313.pyc | Bin 0 -> 12146 bytes reflection_config_generator.ps1 | 464 ++++++++++++++++++ reflection_config_generator.py | 337 +++++++++++++ ...eflection_config_generator.cpython-313.pyc | Bin 0 -> 4083 bytes tests/test_reflection_config_generator.py | 78 +++ 6 files changed, 943 insertions(+) create mode 100644 README.md create mode 100644 __pycache__/reflection_config_generator.cpython-313.pyc create mode 100644 reflection_config_generator.ps1 create mode 100644 reflection_config_generator.py create mode 100644 tests/__pycache__/test_reflection_config_generator.cpython-313.pyc create mode 100644 tests/test_reflection_config_generator.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..9d0b6e6 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# Reflection Config Generator + +Ein kleines CLI-Tool, das Java-Quellcode scannt und daraus eine +`reflection-config.json` fuer Quarkus/GraalVM erzeugt. + +## Voraussetzungen + +- PowerShell 5.1+ oder PowerShell 7+ +- Optional: Python 3.9+ (wenn du die Python-Variante nutzen willst) + +## Nutzung + +### PowerShell (empfohlen) + +```powershell +powershell -ExecutionPolicy Bypass -File .\reflection_config_generator.ps1 ` + -SourceRoot C:\apps\service-a\src\main\java ` + -Package com.example ` + -Output C:\apps\service-a\src\main\resources\META-INF\native-image\reflection-config.json +``` + +Erzeugt standardmaessig `reflection-config.json` im aktuellen Verzeichnis, wenn `-Output` nicht gesetzt ist. + +### Mehrere Source-Roots und Packages + +```powershell +powershell -ExecutionPolicy Bypass -File .\reflection_config_generator.ps1 ` + -SourceRoot C:\apps\service-a\src\main\java, C:\apps\service-b\src\main\java ` + -Package com.company.project, org.shared.* +``` + +### Ausgabe auf stdout + +```powershell +powershell -ExecutionPolicy Bypass -File .\reflection_config_generator.ps1 ` + -SourceRoot C:\apps\service-a\src\main\java ` + -Package com.example ` + -Stdout +``` + +## Wichtige Optionen + +- `-SourceRoot`: Quellverzeichnis, rekursiv gescannt (mehrfach/als Liste nutzbar) +- `-Package`: Package-Praefix(e), z. B. `com.acme` oder `com.acme.*` +- `-Output`: Zielpfad (Standard: `reflection-config.json`) +- `-Stdout`: JSON statt Datei direkt in die Konsole +- `-IncludeDeclared:$true/$false`: Declared-Member-Flags (Standard: true) +- `-IncludePublic:$true/$false`: Public-Member-Flags (Standard: false) +- `-IncludeConstructors:$true/$false` +- `-IncludeMethods:$true/$false` +- `-IncludeFields:$true/$false` +- `-NoSort`: Discovery-Reihenfolge behalten + +## Python-Variante (optional) + +```bash +python reflection_config_generator.py --package com.example +``` + +## Hinweise + +- Das Tool liest `.java`-Dateien (nicht `.class`-Dateien). +- Es werden Top-Level-Typen (`class`, `interface`, `enum`, `record`) erfasst. +- Dateien ohne `package`-Deklaration werden ignoriert. diff --git a/__pycache__/reflection_config_generator.cpython-313.pyc b/__pycache__/reflection_config_generator.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c64d5910348359e0de61961e487c0447d7a4d2f1 GIT binary patch literal 12146 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 literal 0 HcmV?d00001 diff --git a/reflection_config_generator.ps1 b/reflection_config_generator.ps1 new file mode 100644 index 0000000..4b0408e --- /dev/null +++ b/reflection_config_generator.ps1 @@ -0,0 +1,464 @@ +[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 +} diff --git a/reflection_config_generator.py b/reflection_config_generator.py new file mode 100644 index 0000000..03bca1e --- /dev/null +++ b/reflection_config_generator.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python3 +""" +Generate a GraalVM/Quarkus reflection-config.json from Java source files. +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path +from typing import Iterable + + +JAVA_FILE_PATTERN = "*.java" +TYPE_KEYWORDS = {"class", "interface", "enum", "record"} + + +def normalize_package_filter(raw_filter: str) -> str: + normalized = raw_filter.strip() + if not normalized: + return "" + if normalized.endswith(".*"): + normalized = normalized[:-2] + return normalized.rstrip(".") + + +def parse_package_filters(values: Iterable[str]) -> list[str]: + filters: list[str] = [] + for value in values: + for part in value.split(","): + normalized = normalize_package_filter(part) + if normalized: + filters.append(normalized) + return list(dict.fromkeys(filters)) + + +def package_matches(package_name: str, package_filters: list[str]) -> bool: + if not package_filters: + return True + for prefix in package_filters: + if package_name == prefix or package_name.startswith(prefix + "."): + return True + return False + + +def strip_comments_and_literals(source: str) -> str: + result: list[str] = [] + i = 0 + state = "normal" + length = len(source) + + while i < length: + char = source[i] + nxt = source[i + 1] if i + 1 < length else "" + + if state == "normal": + if char == "/" and nxt == "/": + state = "line_comment" + result.append(" ") + i += 2 + continue + if char == "/" and nxt == "*": + state = "block_comment" + result.append(" ") + i += 2 + continue + if char == '"': + state = "string" + result.append(" ") + i += 1 + continue + if char == "'": + state = "char" + result.append(" ") + i += 1 + continue + result.append(char) + i += 1 + continue + + if state == "line_comment": + if char == "\n": + state = "normal" + result.append("\n") + else: + result.append(" ") + i += 1 + continue + + if state == "block_comment": + if char == "*" and nxt == "/": + state = "normal" + result.append(" ") + i += 2 + else: + result.append("\n" if char == "\n" else " ") + i += 1 + continue + + if state == "string": + if char == "\\" and i + 1 < length: + result.append(" ") + i += 2 + continue + if char == '"': + state = "normal" + result.append(" ") + else: + result.append("\n" if char == "\n" else " ") + i += 1 + continue + + if state == "char": + if char == "\\" and i + 1 < length: + result.append(" ") + i += 2 + continue + if char == "'": + state = "normal" + result.append(" ") + else: + result.append("\n" if char == "\n" else " ") + i += 1 + continue + + return "".join(result) + + +def extract_package(cleaned_source: str) -> str | None: + match = re.search( + r"^\s*package\s+([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\s*;", + cleaned_source, + flags=re.MULTILINE, + ) + if not match: + return None + return match.group(1) + + +def extract_top_level_types(cleaned_source: str) -> list[str]: + tokens = list(re.finditer(r"[A-Za-z_][A-Za-z0-9_]*|[{}]", cleaned_source)) + depth = 0 + types: list[str] = [] + i = 0 + + while i < len(tokens): + token = tokens[i].group(0) + if token == "{": + depth += 1 + i += 1 + continue + if token == "}": + depth = max(0, depth - 1) + i += 1 + continue + + if depth == 0 and token in TYPE_KEYWORDS: + if i + 1 < len(tokens): + candidate = tokens[i + 1].group(0) + if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", candidate): + types.append(candidate) + i += 2 + continue + i += 1 + + return list(dict.fromkeys(types)) + + +def discover_classes(java_file: Path, package_filters: list[str]) -> list[str]: + source = java_file.read_text(encoding="utf-8") + cleaned = strip_comments_and_literals(source) + package_name = extract_package(cleaned) + if not package_name or not package_matches(package_name, package_filters): + return [] + classes = extract_top_level_types(cleaned) + return [f"{package_name}.{name}" for name in classes] + + +def collect_java_files(source_roots: list[Path]) -> list[Path]: + java_files: list[Path] = [] + for root in source_roots: + java_files.extend(root.rglob(JAVA_FILE_PATTERN)) + return java_files + + +def build_reflection_entry( + class_name: str, + include_declared: bool, + include_public: bool, + include_constructors: bool, + include_methods: bool, + include_fields: bool, +) -> dict[str, object]: + entry: dict[str, object] = {"name": class_name} + + if include_declared: + if include_constructors: + entry["allDeclaredConstructors"] = True + if include_methods: + entry["allDeclaredMethods"] = True + if include_fields: + entry["allDeclaredFields"] = True + + if include_public: + if include_constructors: + entry["allPublicConstructors"] = True + if include_methods: + entry["allPublicMethods"] = True + if include_fields: + entry["allPublicFields"] = True + + return entry + + +def build_arg_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description=( + "Scan Java source files and generate reflection-config.json " + "for selected packages." + ) + ) + parser.add_argument( + "--source-root", + action="append", + default=[], + help="Source root directory to scan (repeatable). Defaults to current directory.", + ) + parser.add_argument( + "--package", + action="append", + default=[], + help=( + "Package prefix filter, e.g. com.acme or com.acme.* " + "(repeatable or comma-separated)." + ), + ) + parser.add_argument( + "--output", + default="reflection-config.json", + help="Output path for generated JSON (default: reflection-config.json).", + ) + parser.add_argument( + "--stdout", + action="store_true", + help="Print JSON to stdout instead of writing to --output.", + ) + parser.add_argument( + "--include-declared", + action=argparse.BooleanOptionalAction, + default=True, + help="Include declared constructors/methods/fields (default: true).", + ) + parser.add_argument( + "--include-public", + action=argparse.BooleanOptionalAction, + default=False, + help="Include public constructors/methods/fields (default: false).", + ) + parser.add_argument( + "--include-constructors", + action=argparse.BooleanOptionalAction, + default=True, + help="Include constructor metadata (default: true).", + ) + parser.add_argument( + "--include-methods", + action=argparse.BooleanOptionalAction, + default=True, + help="Include method metadata (default: true).", + ) + parser.add_argument( + "--include-fields", + action=argparse.BooleanOptionalAction, + default=True, + help="Include field metadata (default: true).", + ) + parser.add_argument( + "--no-sort", + action="store_true", + help="Keep discovery order instead of sorting by class name.", + ) + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_arg_parser() + args = parser.parse_args(argv) + + source_roots = [Path(p).resolve() for p in args.source_root] or [Path.cwd().resolve()] + missing_roots = [str(root) for root in source_roots if not root.exists()] + if missing_roots: + parser.error("Missing source roots: " + ", ".join(missing_roots)) + + package_filters = parse_package_filters(args.package) + + class_names: list[str] = [] + for java_file in collect_java_files(source_roots): + try: + class_names.extend(discover_classes(java_file, package_filters)) + except UnicodeDecodeError: + print(f"Skipping non-UTF8 file: {java_file}", file=sys.stderr) + except OSError as exc: + print(f"Skipping unreadable file {java_file}: {exc}", file=sys.stderr) + + class_names = list(dict.fromkeys(class_names)) + if not args.no_sort: + class_names.sort() + + reflection_entries = [ + build_reflection_entry( + class_name=class_name, + include_declared=args.include_declared, + include_public=args.include_public, + include_constructors=args.include_constructors, + include_methods=args.include_methods, + include_fields=args.include_fields, + ) + for class_name in class_names + ] + + output_json = json.dumps(reflection_entries, indent=2) + + if args.stdout: + print(output_json) + else: + output_path = Path(args.output).resolve() + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(output_json + "\n", encoding="utf-8") + print(f"Wrote {len(reflection_entries)} entries to {output_path}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/__pycache__/test_reflection_config_generator.cpython-313.pyc b/tests/__pycache__/test_reflection_config_generator.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..499b5983403682864d49328e5695091a29d97cc3 GIT binary patch literal 4083 zcmbVPO>o=B6g$%5*@0 zU4U*(Pi8Wm{wTdP)6}ifOMP^Da&I~Gm}#$sbT9RZvjFiWjk@Q z$w+2iMZ{|v!nn^YVWDMT zs&zf@18K{)9yX6a=MGv%)K0LGA)Fkd7To|lp%-v_JoI?C7nFk} zw8Eys&I^mUsNuX-GW1zPFP2v3u#Smp8RQB!E%S>AC|<}jt$Aoe_ld4&a~Y~T=ql7+ zS@f;(2cR8AS=h(dn8}s~omoPQ{!Vw`mKeaXfNSZ1wWAcbkm73nd83?B^JSbFpO6f) zk|`R-mtlXfCi-KUYbuda90`NgTBrrA^|U%vn&O-QS;4xqyP^^kD-|{Wrn-WaVo9?w zF`XXOG%>Nx{-~;IDb@*?SSvcAmeiz%Ns@DZc4~PEVCJ%!zpfd}WPAJ4mSJei7Tq#T2E03)PBN%Bz4;RK1|+1}UprsfJBw>8npH^X=5q zdtr`2^$vOz-_wXsJcv&?!Yi8`+aFyGZ3Iz7Y=j5w@W5K~!RYx5ScF6fLp7YdzJ0YJZMO9i?^TlJ7p#(Qa||8f?0h*T55` zOI7d+>2?=@s`8{4bT~@s_S;?Jx4YF5oHs~8dR^73cuclPNnep}rgpxB^=es~Hwsv~ zEzL|wr=_+|@L1k8!{J>ePJrT40YY!3v)2Tb74WTc8S9qkCt3-NRyC9KP(%{HWf3)R zDV9-DG<*|ligl}kO}Hikl?P3uO7hqV?j_fSmmJ~ne|RnuC4KOEm!s0XaU_GKWl+Ims*^^GX zNd0F=0S?@Cb(cQ~>y6R3{Oc+A$#J>AL(chgi z0XEJW)^$v3{at{W%nkLXT08JWe3mMmA$bO3XN|8~#q@EQER6NMQGlRYOLi%P*|MiS zg_AoKW(SGKJ zY3MaEo%W(_niz)Va4Th>0A&{AQsxn?Za0=(=2qG({a{zA@9=2p)?8W{dV zIcGl;!rcOznORe{&X<>{``g*>Wgo3k-^^83mXX}_BDiITNqyZ z!9C`oF#eA{L%+N6>l=3m8Y4&Tk)t1dzdmxbK5}+_&$-pPCM9)>{oBOR_1LjL&fT4S z@8)mzeK=>wjy3!C)%R!XvF~p2-onJbUtN0l(%RhJnGY`3zcXzQpL{5s0u7y`uh_#g z4~5e^MyKrI>4(C}?b+)4viFl8_5EoG1|Pr1pxzhj;`smmeTG4OlaJ5R=TF4(FU|dM zpue4+JlDg0+|zq582xx~<{&)$CCJmdCwgu$^dW+ei6FsNmFNrb#T8Z*dao75i7HCj zC{#7t7ZinlceEtC6!1%8T3SiRx}hk9(kdCERSqg=x1yMqYL)VeYC#fs4N_GTP{%2N z@~3+X2qgx$qL None: + filters = rgen.parse_package_filters( + ["com.acme.*, org.foo", "com.acme", " org.foo.bar "] + ) + self.assertEqual(filters, ["com.acme", "org.foo", "org.foo.bar"]) + + def test_extract_top_level_types_ignores_nested_types(self) -> None: + source = """ + package com.acme; + + public class Outer { + class Inner {} + } + + record Value(String v) {} + enum Mode { A, B } + """ + cleaned = rgen.strip_comments_and_literals(source) + self.assertEqual( + rgen.extract_top_level_types(cleaned), + ["Outer", "Value", "Mode"], + ) + + def test_main_generates_reflection_config_for_filtered_packages(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "src/com/acme").mkdir(parents=True) + (root / "src/com/acme/sub").mkdir(parents=True) + (root / "src/org/other").mkdir(parents=True) + + (root / "src/com/acme/A.java").write_text( + "package com.acme; public class A {}", + encoding="utf-8", + ) + (root / "src/com/acme/sub/B.java").write_text( + "package com.acme.sub; public record B(String name) {}", + encoding="utf-8", + ) + (root / "src/org/other/C.java").write_text( + "package org.other; public class C {}", + encoding="utf-8", + ) + + out_file = root / "reflection-config.json" + exit_code = rgen.main( + [ + "--source-root", + str(root / "src"), + "--package", + "com.acme", + "--output", + str(out_file), + ] + ) + self.assertEqual(exit_code, 0) + + content = json.loads(out_file.read_text(encoding="utf-8")) + self.assertEqual( + [entry["name"] for entry in content], + ["com.acme.A", "com.acme.sub.B"], + ) + self.assertTrue(content[0]["allDeclaredMethods"]) + self.assertTrue(content[0]["allDeclaredConstructors"]) + self.assertTrue(content[0]["allDeclaredFields"]) + self.assertNotIn("allPublicMethods", content[0]) + + +if __name__ == "__main__": + unittest.main()