commit b7b4dbeaa0a9cc0e496309ce8ad1da6fe3bf41a0 Author: admrene Date: Fri Apr 24 07:59:08 2026 +0200 Init Commit 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 0000000..c64d591 Binary files /dev/null and b/__pycache__/reflection_config_generator.cpython-313.pyc differ 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 0000000..499b598 Binary files /dev/null and b/tests/__pycache__/test_reflection_config_generator.cpython-313.pyc differ diff --git a/tests/test_reflection_config_generator.py b/tests/test_reflection_config_generator.py new file mode 100644 index 0000000..604b0e3 --- /dev/null +++ b/tests/test_reflection_config_generator.py @@ -0,0 +1,78 @@ +import json +import tempfile +import unittest +from pathlib import Path + +import reflection_config_generator as rgen + + +class ReflectionConfigGeneratorTests(unittest.TestCase): + def test_parse_package_filters_deduplicates_and_normalizes(self) -> 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()