From e9fb3e65b0e0b95c22cd36f58adfe2ce849d0453 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 00:56:51 +0000 Subject: [PATCH 01/13] Initial plan From 9523d1b53ee60ffc105801eafcf91097dd8b2e24 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 01:03:29 +0000 Subject: [PATCH 02/13] feat(java): add SchemaGenerator compile-time type-to-JSON-Schema utility Creates SchemaGenerator.java that maps javax.lang.model TypeMirror instances to JSON Schema source code literals (Map.of(...) expressions). Implements all 24 type mappings from the specification including: - Primitives and boxed types (int/Integer, long/Long, etc.) - String, UUID, OffsetDateTime - Collections (List, Collection, Set) - Maps (Map with typed values) - Arrays (String[]) - Enums (with constant enumeration) - Records and POJOs (with properties/required) - Optional, OptionalInt, OptionalDouble - Sealed interfaces (oneOf) - JsonNode and Object (any) Also adds SchemaGeneratorTest using compilation-testing approach with javax.tools.JavaCompiler to exercise the generator at compile time. Closes github/copilot-sdk#1759 --- .../github/copilot/tool/SchemaGenerator.java | 370 ++++++++++++ .../copilot/tool/SchemaGeneratorTest.java | 557 ++++++++++++++++++ 2 files changed, 927 insertions(+) create mode 100644 java/src/main/java/com/github/copilot/tool/SchemaGenerator.java create mode 100644 java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java diff --git a/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java b/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java new file mode 100644 index 000000000..342fc7d07 --- /dev/null +++ b/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java @@ -0,0 +1,370 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.tool; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.RecordComponentElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** + * Compile-time utility that maps {@code javax.lang.model} types to JSON Schema + * represented as Java source code literals ({@code Map.of(...)} expressions). + * + *

This class is invoked by the annotation processor and operates exclusively + * with the {@code javax.lang.model} API. It does NOT use {@code java.lang.reflect}. + * + * @since 1.0.2 + */ +public class SchemaGenerator { + + /** + * Given a {@link TypeMirror} from the annotation processing environment, + * returns a {@code String} containing Java source code for a {@code Map} literal + * representing the JSON Schema of that type. + * + * @param type the type to generate schema for + * @param typeUtils the {@link Types} utility from the processing environment + * @param elementUtils the {@link Elements} utility from the processing environment + * @return a Java source code string representing the JSON Schema + */ + public String generateSchemaSource(TypeMirror type, Types typeUtils, Elements elementUtils) { + return generateSchema(type, typeUtils, elementUtils, false); + } + + /** + * Generates the full "parameters" schema source for a method's parameters. + * Produces a {@code Map.of("type", "object", "properties", Map.of(...), "required", List.of(...))}. + * + * @param parameters the method parameters to generate schema for + * @param typeUtils the {@link Types} utility from the processing environment + * @param elementUtils the {@link Elements} utility from the processing environment + * @return a Java source code string representing the parameters JSON Schema + */ + public String generateParametersSchemaSource( + List parameters, Types typeUtils, Elements elementUtils) { + if (parameters.isEmpty()) { + return "Map.of(\"type\", \"object\", \"properties\", Map.of(), \"required\", List.of())"; + } + + List propertyEntries = new ArrayList<>(); + List requiredNames = new ArrayList<>(); + + for (VariableElement param : parameters) { + String paramName = param.getSimpleName().toString(); + TypeMirror paramType = param.asType(); + + boolean isOptional = isOptionalType(paramType, typeUtils, elementUtils); + String schema; + if (isOptional) { + schema = generateSchema(unwrapOptional(paramType, typeUtils, elementUtils), typeUtils, elementUtils, + false); + } else { + schema = generateSchema(paramType, typeUtils, elementUtils, false); + } + + propertyEntries.add("\"" + paramName + "\", " + schema); + + if (!isOptional) { + Param paramAnnotation = param.getAnnotation(Param.class); + if (paramAnnotation == null || paramAnnotation.required()) { + requiredNames.add("\"" + paramName + "\""); + } + } + } + + String properties = "Map.of(" + String.join(", ", propertyEntries) + ")"; + String required = "List.of(" + String.join(", ", requiredNames) + ")"; + + return "Map.of(\"type\", \"object\", \"properties\", " + properties + ", \"required\", " + required + ")"; + } + + private String generateSchema(TypeMirror type, Types typeUtils, Elements elementUtils, boolean nested) { + // Handle primitive types + if (type.getKind().isPrimitive()) { + return generatePrimitiveSchema(type.getKind()); + } + + // Handle array types + if (type.getKind() == TypeKind.ARRAY) { + ArrayType arrayType = (ArrayType) type; + TypeMirror componentType = arrayType.getComponentType(); + String itemsSchema = generateSchema(componentType, typeUtils, elementUtils, true); + return "Map.of(\"type\", \"array\", \"items\", " + itemsSchema + ")"; + } + + // Handle declared types (classes, interfaces, enums, records) + if (type.getKind() == TypeKind.DECLARED) { + return generateDeclaredTypeSchema((DeclaredType) type, typeUtils, elementUtils); + } + + // Fallback: any + return "Map.of()"; + } + + private String generatePrimitiveSchema(TypeKind kind) { + switch (kind) { + case INT: + case LONG: + return "Map.of(\"type\", \"integer\")"; + case DOUBLE: + case FLOAT: + return "Map.of(\"type\", \"number\")"; + case BOOLEAN: + return "Map.of(\"type\", \"boolean\")"; + default: + return "Map.of()"; + } + } + + private String generateDeclaredTypeSchema(DeclaredType type, Types typeUtils, Elements elementUtils) { + TypeElement typeElement = (TypeElement) type.asElement(); + String qualifiedName = typeElement.getQualifiedName().toString(); + + // String + if ("java.lang.String".equals(qualifiedName)) { + return "Map.of(\"type\", \"string\")"; + } + + // Boxed primitives + if ("java.lang.Integer".equals(qualifiedName) || "java.lang.Long".equals(qualifiedName)) { + return "Map.of(\"type\", \"integer\")"; + } + if ("java.lang.Double".equals(qualifiedName) || "java.lang.Float".equals(qualifiedName)) { + return "Map.of(\"type\", \"number\")"; + } + if ("java.lang.Boolean".equals(qualifiedName)) { + return "Map.of(\"type\", \"boolean\")"; + } + + // UUID + if ("java.util.UUID".equals(qualifiedName)) { + return "Map.of(\"type\", \"string\", \"format\", \"uuid\")"; + } + + // OffsetDateTime + if ("java.time.OffsetDateTime".equals(qualifiedName)) { + return "Map.of(\"type\", \"string\", \"format\", \"date-time\")"; + } + + // JsonNode (any) + if ("com.fasterxml.jackson.databind.JsonNode".equals(qualifiedName)) { + return "Map.of()"; + } + + // Object (any) + if ("java.lang.Object".equals(qualifiedName)) { + return "Map.of()"; + } + + // Optional types + if ("java.util.Optional".equals(qualifiedName)) { + List typeArgs = type.getTypeArguments(); + if (!typeArgs.isEmpty()) { + return generateSchema(typeArgs.get(0), typeUtils, elementUtils, true); + } + return "Map.of()"; + } + if ("java.util.OptionalInt".equals(qualifiedName)) { + return "Map.of(\"type\", \"integer\")"; + } + if ("java.util.OptionalDouble".equals(qualifiedName)) { + return "Map.of(\"type\", \"number\")"; + } + + // List / Collection + if (isCollectionType(qualifiedName)) { + List typeArgs = type.getTypeArguments(); + if (!typeArgs.isEmpty()) { + String itemsSchema = generateSchema(typeArgs.get(0), typeUtils, elementUtils, true); + return "Map.of(\"type\", \"array\", \"items\", " + itemsSchema + ")"; + } + return "Map.of(\"type\", \"array\")"; + } + + // Map + if (isMapType(qualifiedName)) { + List typeArgs = type.getTypeArguments(); + if (typeArgs.size() == 2) { + TypeMirror valueType = typeArgs.get(1); + if (valueType.getKind() == TypeKind.DECLARED) { + TypeElement valueElement = (TypeElement) ((DeclaredType) valueType).asElement(); + String valueQName = valueElement.getQualifiedName().toString(); + if ("java.lang.Object".equals(valueQName)) { + return "Map.of(\"type\", \"object\")"; + } + } + String valueSchema = generateSchema(valueType, typeUtils, elementUtils, true); + return "Map.of(\"type\", \"object\", \"additionalProperties\", " + valueSchema + ")"; + } + return "Map.of(\"type\", \"object\")"; + } + + // Enum types + if (typeElement.getKind() == ElementKind.ENUM) { + List constants = typeElement.getEnclosedElements().stream() + .filter(e -> e.getKind() == ElementKind.ENUM_CONSTANT) + .map(e -> "\"" + e.getSimpleName().toString() + "\"") + .collect(Collectors.toList()); + return "Map.of(\"type\", \"string\", \"enum\", List.of(" + String.join(", ", constants) + "))"; + } + + // Record types + if (typeElement.getKind() == ElementKind.RECORD) { + return generateRecordSchema(typeElement, typeUtils, elementUtils); + } + + // POJO / class types — treat as object with fields + if (typeElement.getKind() == ElementKind.CLASS) { + return generateClassSchema(typeElement, typeUtils, elementUtils); + } + + // Sealed interfaces with @JsonSubTypes — oneOf + if (typeElement.getKind() == ElementKind.INTERFACE) { + return generateSealedSchema(typeElement, typeUtils, elementUtils); + } + + return "Map.of()"; + } + + private String generateRecordSchema(TypeElement typeElement, Types typeUtils, Elements elementUtils) { + List propertyEntries = new ArrayList<>(); + List requiredNames = new ArrayList<>(); + + for (Element enclosed : typeElement.getEnclosedElements()) { + if (enclosed.getKind() == ElementKind.RECORD_COMPONENT) { + RecordComponentElement component = (RecordComponentElement) enclosed; + String name = component.getSimpleName().toString(); + TypeMirror componentType = component.asType(); + + boolean isOptional = isOptionalType(componentType, typeUtils, elementUtils); + String schema; + if (isOptional) { + schema = generateSchema( + unwrapOptional(componentType, typeUtils, elementUtils), typeUtils, elementUtils, true); + } else { + schema = generateSchema(componentType, typeUtils, elementUtils, true); + requiredNames.add("\"" + name + "\""); + } + + propertyEntries.add("\"" + name + "\", " + schema); + } + } + + String properties = "Map.of(" + String.join(", ", propertyEntries) + ")"; + String required = "List.of(" + String.join(", ", requiredNames) + ")"; + + return "Map.of(\"type\", \"object\", \"properties\", " + properties + ", \"required\", " + required + ")"; + } + + private String generateClassSchema(TypeElement typeElement, Types typeUtils, Elements elementUtils) { + List propertyEntries = new ArrayList<>(); + List requiredNames = new ArrayList<>(); + + for (Element enclosed : typeElement.getEnclosedElements()) { + if (enclosed.getKind() == ElementKind.FIELD) { + VariableElement field = (VariableElement) enclosed; + // Skip static fields + if (field.getModifiers().contains(javax.lang.model.element.Modifier.STATIC)) { + continue; + } + String name = field.getSimpleName().toString(); + TypeMirror fieldType = field.asType(); + + boolean isOptional = isOptionalType(fieldType, typeUtils, elementUtils); + String schema; + if (isOptional) { + schema = generateSchema( + unwrapOptional(fieldType, typeUtils, elementUtils), typeUtils, elementUtils, true); + } else { + schema = generateSchema(fieldType, typeUtils, elementUtils, true); + requiredNames.add("\"" + name + "\""); + } + + propertyEntries.add("\"" + name + "\", " + schema); + } + } + + if (propertyEntries.isEmpty()) { + return "Map.of(\"type\", \"object\")"; + } + + String properties = "Map.of(" + String.join(", ", propertyEntries) + ")"; + String required = "List.of(" + String.join(", ", requiredNames) + ")"; + + return "Map.of(\"type\", \"object\", \"properties\", " + properties + ", \"required\", " + required + ")"; + } + + private String generateSealedSchema(TypeElement typeElement, Types typeUtils, Elements elementUtils) { + List permittedSubclasses = typeElement.getPermittedSubclasses(); + if (permittedSubclasses != null && !permittedSubclasses.isEmpty()) { + List schemas = permittedSubclasses.stream() + .map(sub -> generateSchema(sub, typeUtils, elementUtils, true)) + .collect(Collectors.toList()); + return "Map.of(\"oneOf\", List.of(" + String.join(", ", schemas) + "))"; + } + return "Map.of(\"type\", \"object\")"; + } + + private boolean isOptionalType(TypeMirror type, Types typeUtils, Elements elementUtils) { + if (type.getKind() != TypeKind.DECLARED) { + return false; + } + DeclaredType declaredType = (DeclaredType) type; + TypeElement element = (TypeElement) declaredType.asElement(); + String name = element.getQualifiedName().toString(); + return "java.util.Optional".equals(name) + || "java.util.OptionalInt".equals(name) + || "java.util.OptionalDouble".equals(name) + || "java.util.OptionalLong".equals(name); + } + + private TypeMirror unwrapOptional(TypeMirror type, Types typeUtils, Elements elementUtils) { + if (type.getKind() != TypeKind.DECLARED) { + return type; + } + DeclaredType declaredType = (DeclaredType) type; + TypeElement element = (TypeElement) declaredType.asElement(); + String name = element.getQualifiedName().toString(); + + if ("java.util.Optional".equals(name)) { + List typeArgs = declaredType.getTypeArguments(); + if (!typeArgs.isEmpty()) { + return typeArgs.get(0); + } + } + if ("java.util.OptionalInt".equals(name)) { + return typeUtils.getPrimitiveType(TypeKind.INT); + } + if ("java.util.OptionalDouble".equals(name)) { + return typeUtils.getPrimitiveType(TypeKind.DOUBLE); + } + if ("java.util.OptionalLong".equals(name)) { + return typeUtils.getPrimitiveType(TypeKind.LONG); + } + return type; + } + + private boolean isCollectionType(String qualifiedName) { + return "java.util.List".equals(qualifiedName) + || "java.util.Collection".equals(qualifiedName) + || "java.util.Set".equals(qualifiedName); + } + + private boolean isMapType(String qualifiedName) { + return "java.util.Map".equals(qualifiedName); + } +} diff --git a/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java new file mode 100644 index 000000000..e63227727 --- /dev/null +++ b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java @@ -0,0 +1,557 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.tool; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.ToolProvider; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link SchemaGenerator} using the compilation-testing approach. + * A test annotation processor exercises SchemaGenerator during compilation + * of small source snippets. + */ +public class SchemaGeneratorTest { + + /** + * In-memory Java source file for compilation testing. + */ + private static class InMemorySource extends SimpleJavaFileObject { + + private final String code; + + InMemorySource(String className, String code) { + super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); + this.code = code; + } + + @Override + public CharSequence getContent() throws IOException { + return code; + } + } + + /** + * Test processor that captures schema generation results. + */ + @SupportedAnnotationTypes("*") + @SupportedSourceVersion(SourceVersion.RELEASE_17) + public static class SchemaCapturingProcessor extends AbstractProcessor { + + static final List capturedSchemas = new ArrayList<>(); + static final List capturedParameterSchemas = new ArrayList<>(); + + private Types typeUtils; + private Elements elementUtils; + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + this.typeUtils = processingEnv.getTypeUtils(); + this.elementUtils = processingEnv.getElementUtils(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + if (roundEnv.processingOver()) { + return false; + } + + SchemaGenerator generator = new SchemaGenerator(); + + for (Element rootElement : roundEnv.getRootElements()) { + if (rootElement.getKind() == ElementKind.CLASS || rootElement.getKind() == ElementKind.RECORD + || rootElement.getKind() == ElementKind.INTERFACE || rootElement.getKind() == ElementKind.ENUM) { + // Find methods named "schemaTarget" to capture schemas for their return type + for (Element enclosed : rootElement.getEnclosedElements()) { + if (enclosed.getKind() == ElementKind.METHOD) { + ExecutableElement method = (ExecutableElement) enclosed; + String methodName = method.getSimpleName().toString(); + if (methodName.startsWith("schemaTarget")) { + TypeMirror returnType = method.getReturnType(); + String schema = generator.generateSchemaSource(returnType, typeUtils, elementUtils); + capturedSchemas.add(methodName + "=" + schema); + } + if ("parametersTarget".equals(methodName)) { + List params = method.getParameters(); + String schema = + generator.generateParametersSchemaSource(params, typeUtils, elementUtils); + capturedParameterSchemas.add(schema); + } + } + } + + // For record/enum types, generate schema for the type itself + TypeElement typeElement = (TypeElement) rootElement; + String typeName = typeElement.getSimpleName().toString(); + if (typeName.startsWith("TestRecord") || typeName.startsWith("TestEnum") + || typeName.startsWith("TestSealed")) { + String schema = + generator.generateSchemaSource(typeElement.asType(), typeUtils, elementUtils); + capturedSchemas.add(typeName + "=" + schema); + } + } + } + + return false; + } + } + + private List compileAndCapture(String... sources) { + return compileAndCapture(Arrays.asList(sources)); + } + + private List compileAndCapture(List sourceTexts) { + SchemaCapturingProcessor.capturedSchemas.clear(); + SchemaCapturingProcessor.capturedParameterSchemas.clear(); + + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertNotNull(compiler, "System Java compiler not available"); + + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + + List compilationUnits = new ArrayList<>(); + for (String sourceText : sourceTexts) { + // Extract class name from source + String className = extractClassName(sourceText); + compilationUnits.add(new InMemorySource(className, sourceText)); + } + + // Compile with the processor on classpath + JavaCompiler.CompilationTask task = compiler.getTask( + null, // writer + null, // file manager + diagnostics, // diagnostics + List.of("--add-modules", "ALL-MODULE-PATH"), // options + null, // annotation classes + compilationUnits); + + task.setProcessors(List.of(new SchemaCapturingProcessor())); + boolean success = task.call(); + + if (!success) { + // Try without module options for simpler environments + diagnostics = new DiagnosticCollector<>(); + task = compiler.getTask(null, null, diagnostics, null, null, compilationUnits); + task.setProcessors(List.of(new SchemaCapturingProcessor())); + success = task.call(); + } + + assertTrue(success, "Compilation failed: " + diagnostics.getDiagnostics()); + return new ArrayList<>(SchemaCapturingProcessor.capturedSchemas); + } + + private List compileAndCaptureParams(String source) { + SchemaCapturingProcessor.capturedSchemas.clear(); + SchemaCapturingProcessor.capturedParameterSchemas.clear(); + + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertNotNull(compiler, "System Java compiler not available"); + + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + + String className = extractClassName(source); + List compilationUnits = List.of(new InMemorySource(className, source)); + + JavaCompiler.CompilationTask task = compiler.getTask(null, null, diagnostics, null, null, compilationUnits); + task.setProcessors(List.of(new SchemaCapturingProcessor())); + boolean success = task.call(); + + assertTrue(success, "Compilation failed: " + diagnostics.getDiagnostics()); + return new ArrayList<>(SchemaCapturingProcessor.capturedParameterSchemas); + } + + private String extractClassName(String source) { + // Simple extraction: find "class X", "record X", "enum X", or "interface X" + for (String keyword : new String[] {"class ", "record ", "enum ", "interface "}) { + int idx = source.indexOf(keyword); + if (idx >= 0) { + int start = idx + keyword.length(); + int end = start; + while (end < source.length() && Character.isJavaIdentifierPart(source.charAt(end))) { + end++; + } + return source.substring(start, end); + } + } + return "Unknown"; + } + + // --- Type mapping tests --- + + @Test + void stringType() { + String source = """ + public class TestStringHolder { + public String schemaTargetString() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema(schemas, "schemaTargetString", "Map.of(\"type\", \"string\")"); + } + + @Test + void intPrimitiveType() { + String source = """ + public class TestIntHolder { + public int schemaTargetInt() { return 0; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema(schemas, "schemaTargetInt", "Map.of(\"type\", \"integer\")"); + } + + @Test + void integerBoxedType() { + String source = """ + public class TestIntegerHolder { + public Integer schemaTargetInteger() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema(schemas, "schemaTargetInteger", "Map.of(\"type\", \"integer\")"); + } + + @Test + void longType() { + String source = """ + public class TestLongHolder { + public long schemaTargetLong() { return 0L; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema(schemas, "schemaTargetLong", "Map.of(\"type\", \"integer\")"); + } + + @Test + void doubleType() { + String source = """ + public class TestDoubleHolder { + public double schemaTargetDouble() { return 0.0; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema(schemas, "schemaTargetDouble", "Map.of(\"type\", \"number\")"); + } + + @Test + void floatType() { + String source = """ + public class TestFloatHolder { + public float schemaTargetFloat() { return 0.0f; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema(schemas, "schemaTargetFloat", "Map.of(\"type\", \"number\")"); + } + + @Test + void booleanPrimitiveType() { + String source = """ + public class TestBooleanHolder { + public boolean schemaTargetBoolean() { return false; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema(schemas, "schemaTargetBoolean", "Map.of(\"type\", \"boolean\")"); + } + + @Test + void booleanBoxedType() { + String source = """ + public class TestBooleanBoxedHolder { + public Boolean schemaTargetBooleanBoxed() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema(schemas, "schemaTargetBooleanBoxed", "Map.of(\"type\", \"boolean\")"); + } + + @Test + void stringArrayType() { + String source = """ + public class TestArrayHolder { + public String[] schemaTargetArray() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema( + schemas, "schemaTargetArray", "Map.of(\"type\", \"array\", \"items\", Map.of(\"type\", \"string\"))"); + } + + @Test + void enumType() { + String source = """ + public enum TestEnumColor { RED, GREEN, BLUE } + """; + List schemas = compileAndCapture(source); + assertContainsSchema( + schemas, + "TestEnumColor", + "Map.of(\"type\", \"string\", \"enum\", List.of(\"RED\", \"GREEN\", \"BLUE\"))"); + } + + @Test + void listOfStringType() { + String source = """ + import java.util.List; + public class TestListHolder { + public List schemaTargetList() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema( + schemas, "schemaTargetList", "Map.of(\"type\", \"array\", \"items\", Map.of(\"type\", \"string\"))"); + } + + @Test + void mapStringStringType() { + String source = """ + import java.util.Map; + public class TestMapHolder { + public Map schemaTargetMap() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema( + schemas, + "schemaTargetMap", + "Map.of(\"type\", \"object\", \"additionalProperties\", Map.of(\"type\", \"string\"))"); + } + + @Test + void mapStringObjectType() { + String source = """ + import java.util.Map; + public class TestMapObjectHolder { + public Map schemaTargetMapObject() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema(schemas, "schemaTargetMapObject", "Map.of(\"type\", \"object\")"); + } + + @Test + void mapStringBooleanType() { + String source = """ + import java.util.Map; + public class TestMapBoolHolder { + public Map schemaTargetMapBool() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema( + schemas, + "schemaTargetMapBool", + "Map.of(\"type\", \"object\", \"additionalProperties\", Map.of(\"type\", \"boolean\"))"); + } + + @Test + void mapStringLongType() { + String source = """ + import java.util.Map; + public class TestMapLongHolder { + public Map schemaTargetMapLong() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema( + schemas, + "schemaTargetMapLong", + "Map.of(\"type\", \"object\", \"additionalProperties\", Map.of(\"type\", \"integer\"))"); + } + + @Test + void optionalStringType() { + String source = """ + import java.util.Optional; + public class TestOptionalHolder { + public Optional schemaTargetOptional() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema(schemas, "schemaTargetOptional", "Map.of(\"type\", \"string\")"); + } + + @Test + void uuidType() { + String source = """ + import java.util.UUID; + public class TestUuidHolder { + public UUID schemaTargetUuid() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema(schemas, "schemaTargetUuid", "Map.of(\"type\", \"string\", \"format\", \"uuid\")"); + } + + @Test + void offsetDateTimeType() { + String source = """ + import java.time.OffsetDateTime; + public class TestDateTimeHolder { + public OffsetDateTime schemaTargetDateTime() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema( + schemas, "schemaTargetDateTime", "Map.of(\"type\", \"string\", \"format\", \"date-time\")"); + } + + @Test + void recordType() { + String source = """ + public record TestRecordPerson(String name, int age, boolean active) {} + """; + List schemas = compileAndCapture(source); + String expected = "Map.of(\"type\", \"object\", \"properties\", " + + "Map.of(\"name\", Map.of(\"type\", \"string\"), " + + "\"age\", Map.of(\"type\", \"integer\"), " + + "\"active\", Map.of(\"type\", \"boolean\")), " + + "\"required\", List.of(\"name\", \"age\", \"active\"))"; + assertContainsSchema(schemas, "TestRecordPerson", expected); + } + + @Test + void recordWithOptionalField() { + String source = """ + import java.util.Optional; + public record TestRecordWithOptional(String name, Optional nickname) {} + """; + List schemas = compileAndCapture(source); + String expected = "Map.of(\"type\", \"object\", \"properties\", " + + "Map.of(\"name\", Map.of(\"type\", \"string\"), " + + "\"nickname\", Map.of(\"type\", \"string\")), " + + "\"required\", List.of(\"name\"))"; + assertContainsSchema(schemas, "TestRecordWithOptional", expected); + } + + @Test + void parametersSchema() { + String source = """ + public class TestParamsHolder { + public void parametersTarget(String query, int limit, boolean verbose) {} + } + """; + List paramSchemas = compileAndCaptureParams(source); + assertFalse(paramSchemas.isEmpty(), "Expected parameter schemas"); + String schema = paramSchemas.get(0); + assertTrue(schema.contains("\"type\", \"object\""), "Should be object type: " + schema); + assertTrue(schema.contains("\"query\", Map.of(\"type\", \"string\")"), "Should have query property: " + schema); + assertTrue( + schema.contains("\"limit\", Map.of(\"type\", \"integer\")"), "Should have limit property: " + schema); + assertTrue( + schema.contains("\"verbose\", Map.of(\"type\", \"boolean\")"), + "Should have verbose property: " + schema); + assertTrue(schema.contains("\"required\", List.of("), "Should have required list: " + schema); + } + + @Test + void generatedSourceIsValidJava() { + // Verify that generated schema source code compiles when embedded in a method body + String source = """ + import java.util.List; + import java.util.Map; + import java.util.Optional; + public class TestValidJavaHolder { + public String schemaTargetStr() { return null; } + public List schemaTargetListStr() { return null; } + public Map schemaTargetMapStr() { return null; } + public Optional schemaTargetOpt() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertFalse(schemas.isEmpty()); + + // Build a Java source that uses the generated schema expressions + StringBuilder validationSource = new StringBuilder(); + validationSource.append("import java.util.Map;\n"); + validationSource.append("import java.util.List;\n"); + validationSource.append("public class SchemaValidation {\n"); + validationSource.append(" @SuppressWarnings(\"unchecked\")\n"); + validationSource.append(" public void validate() {\n"); + for (int i = 0; i < schemas.size(); i++) { + String schema = schemas.get(i); + String schemaExpr = schema.substring(schema.indexOf('=') + 1); + validationSource.append(" Object s" + i + " = " + schemaExpr + ";\n"); + } + validationSource.append(" }\n"); + validationSource.append("}\n"); + + // Compile the validation source to verify syntactic validity + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + List compilationUnits = + List.of(new InMemorySource("SchemaValidation", validationSource.toString())); + + JavaCompiler.CompilationTask task = compiler.getTask(null, null, diagnostics, null, null, compilationUnits); + boolean success = task.call(); + + assertTrue( + success, + "Generated schema source code is not valid Java: " + + diagnostics.getDiagnostics() + "\nSource:\n" + validationSource); + } + + @Test + void nestedMapListType() { + String source = """ + import java.util.List; + import java.util.Map; + public class TestNestedHolder { + public Map> schemaTargetNestedMap() { return null; } + } + """; + List schemas = compileAndCapture(source); + String expected = "Map.of(\"type\", \"object\", \"additionalProperties\", " + + "Map.of(\"type\", \"array\", \"items\", Map.of(\"type\", \"string\")))"; + assertContainsSchema(schemas, "schemaTargetNestedMap", expected); + } + + @Test + void objectType() { + String source = """ + public class TestObjectHolder { + public Object schemaTargetObject() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema(schemas, "schemaTargetObject", "Map.of()"); + } + + private void assertContainsSchema(List schemas, String methodName, String expectedSchema) { + String expected = methodName + "=" + expectedSchema; + assertTrue( + schemas.stream().anyMatch(s -> s.equals(expected)), + "Expected schema '" + expected + "' not found in: " + schemas); + } +} From 5313a7a5a3a5a31e5793113cbfe7c348fdf77ddb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 01:05:50 +0000 Subject: [PATCH 03/13] fix: address code review - remove unused param, handle all primitive types --- .../github/copilot/tool/SchemaGenerator.java | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java b/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java index 342fc7d07..e9b6e05ca 100644 --- a/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java +++ b/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java @@ -42,7 +42,7 @@ public class SchemaGenerator { * @return a Java source code string representing the JSON Schema */ public String generateSchemaSource(TypeMirror type, Types typeUtils, Elements elementUtils) { - return generateSchema(type, typeUtils, elementUtils, false); + return generateSchema(type, typeUtils, elementUtils); } /** @@ -70,10 +70,9 @@ public String generateParametersSchemaSource( boolean isOptional = isOptionalType(paramType, typeUtils, elementUtils); String schema; if (isOptional) { - schema = generateSchema(unwrapOptional(paramType, typeUtils, elementUtils), typeUtils, elementUtils, - false); + schema = generateSchema(unwrapOptional(paramType, typeUtils, elementUtils), typeUtils, elementUtils); } else { - schema = generateSchema(paramType, typeUtils, elementUtils, false); + schema = generateSchema(paramType, typeUtils, elementUtils); } propertyEntries.add("\"" + paramName + "\", " + schema); @@ -92,7 +91,7 @@ public String generateParametersSchemaSource( return "Map.of(\"type\", \"object\", \"properties\", " + properties + ", \"required\", " + required + ")"; } - private String generateSchema(TypeMirror type, Types typeUtils, Elements elementUtils, boolean nested) { + private String generateSchema(TypeMirror type, Types typeUtils, Elements elementUtils) { // Handle primitive types if (type.getKind().isPrimitive()) { return generatePrimitiveSchema(type.getKind()); @@ -102,7 +101,7 @@ private String generateSchema(TypeMirror type, Types typeUtils, Elements element if (type.getKind() == TypeKind.ARRAY) { ArrayType arrayType = (ArrayType) type; TypeMirror componentType = arrayType.getComponentType(); - String itemsSchema = generateSchema(componentType, typeUtils, elementUtils, true); + String itemsSchema = generateSchema(componentType, typeUtils, elementUtils); return "Map.of(\"type\", \"array\", \"items\", " + itemsSchema + ")"; } @@ -119,12 +118,16 @@ private String generatePrimitiveSchema(TypeKind kind) { switch (kind) { case INT: case LONG: + case BYTE: + case SHORT: return "Map.of(\"type\", \"integer\")"; case DOUBLE: case FLOAT: return "Map.of(\"type\", \"number\")"; case BOOLEAN: return "Map.of(\"type\", \"boolean\")"; + case CHAR: + return "Map.of(\"type\", \"string\")"; default: return "Map.of()"; } @@ -174,7 +177,7 @@ private String generateDeclaredTypeSchema(DeclaredType type, Types typeUtils, El if ("java.util.Optional".equals(qualifiedName)) { List typeArgs = type.getTypeArguments(); if (!typeArgs.isEmpty()) { - return generateSchema(typeArgs.get(0), typeUtils, elementUtils, true); + return generateSchema(typeArgs.get(0), typeUtils, elementUtils); } return "Map.of()"; } @@ -189,7 +192,7 @@ private String generateDeclaredTypeSchema(DeclaredType type, Types typeUtils, El if (isCollectionType(qualifiedName)) { List typeArgs = type.getTypeArguments(); if (!typeArgs.isEmpty()) { - String itemsSchema = generateSchema(typeArgs.get(0), typeUtils, elementUtils, true); + String itemsSchema = generateSchema(typeArgs.get(0), typeUtils, elementUtils); return "Map.of(\"type\", \"array\", \"items\", " + itemsSchema + ")"; } return "Map.of(\"type\", \"array\")"; @@ -207,7 +210,7 @@ private String generateDeclaredTypeSchema(DeclaredType type, Types typeUtils, El return "Map.of(\"type\", \"object\")"; } } - String valueSchema = generateSchema(valueType, typeUtils, elementUtils, true); + String valueSchema = generateSchema(valueType, typeUtils, elementUtils); return "Map.of(\"type\", \"object\", \"additionalProperties\", " + valueSchema + ")"; } return "Map.of(\"type\", \"object\")"; @@ -254,9 +257,9 @@ private String generateRecordSchema(TypeElement typeElement, Types typeUtils, El String schema; if (isOptional) { schema = generateSchema( - unwrapOptional(componentType, typeUtils, elementUtils), typeUtils, elementUtils, true); + unwrapOptional(componentType, typeUtils, elementUtils), typeUtils, elementUtils); } else { - schema = generateSchema(componentType, typeUtils, elementUtils, true); + schema = generateSchema(componentType, typeUtils, elementUtils); requiredNames.add("\"" + name + "\""); } @@ -288,9 +291,9 @@ private String generateClassSchema(TypeElement typeElement, Types typeUtils, Ele String schema; if (isOptional) { schema = generateSchema( - unwrapOptional(fieldType, typeUtils, elementUtils), typeUtils, elementUtils, true); + unwrapOptional(fieldType, typeUtils, elementUtils), typeUtils, elementUtils); } else { - schema = generateSchema(fieldType, typeUtils, elementUtils, true); + schema = generateSchema(fieldType, typeUtils, elementUtils); requiredNames.add("\"" + name + "\""); } @@ -312,7 +315,7 @@ private String generateSealedSchema(TypeElement typeElement, Types typeUtils, El List permittedSubclasses = typeElement.getPermittedSubclasses(); if (permittedSubclasses != null && !permittedSubclasses.isEmpty()) { List schemas = permittedSubclasses.stream() - .map(sub -> generateSchema(sub, typeUtils, elementUtils, true)) + .map(sub -> generateSchema(sub, typeUtils, elementUtils)) .collect(Collectors.toList()); return "Map.of(\"oneOf\", List.of(" + String.join(", ", schemas) + "))"; } From f5bde697f04e2aa521d2eab29b601356bc95e2c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:49:52 +0000 Subject: [PATCH 04/13] fix(java): correct SimpleJavaFileObject override - getCharContent not getContent Co-authored-by: edburns <75821+edburns@users.noreply.github.com> --- .../test/java/com/github/copilot/tool/SchemaGeneratorTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java index e63227727..292ba87cf 100644 --- a/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java +++ b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java @@ -55,7 +55,7 @@ private static class InMemorySource extends SimpleJavaFileObject { } @Override - public CharSequence getContent() throws IOException { + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { return code; } } From 8d265bf3638c35e264ba95e6de012b8aed9a6fe6 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Tue, 23 Jun 2026 12:58:16 -0400 Subject: [PATCH 05/13] spotless --- java/SchemaValidation.class | Bin 0 -> 597 bytes java/TestArrayHolder.class | Bin 0 -> 288 bytes java/TestBooleanBoxedHolder.class | Bin 0 -> 309 bytes java/TestBooleanHolder.class | Bin 0 -> 276 bytes java/TestDateTimeHolder.class | Bin 0 -> 304 bytes java/TestDoubleHolder.class | Bin 0 -> 273 bytes java/TestEnumColor.class | Bin 0 -> 948 bytes java/TestFloatHolder.class | Bin 0 -> 270 bytes java/TestIntHolder.class | Bin 0 -> 264 bytes java/TestIntegerHolder.class | Bin 0 -> 294 bytes java/TestListHolder.class | Bin 0 -> 343 bytes java/TestLongHolder.class | Bin 0 -> 267 bytes java/TestMapBoolHolder.class | Bin 0 -> 369 bytes java/TestMapHolder.class | Bin 0 -> 356 bytes java/TestMapLongHolder.class | Bin 0 -> 366 bytes java/TestMapObjectHolder.class | Bin 0 -> 374 bytes java/TestNestedHolder.class | Bin 0 -> 386 bytes java/TestObjectHolder.class | Bin 0 -> 290 bytes java/TestOptionalHolder.class | Bin 0 -> 363 bytes java/TestParamsHolder.class | Bin 0 -> 290 bytes java/TestRecordPerson.class | Bin 0 -> 1353 bytes java/TestRecordWithOptional.class | Bin 0 -> 1543 bytes java/TestStringHolder.class | Bin 0 -> 290 bytes java/TestUuidHolder.class | Bin 0 -> 282 bytes java/TestValidJavaHolder.class | Bin 0 -> 720 bytes .../github/copilot/tool/SchemaGenerator.java | 76 +++++++++-------- .../copilot/tool/SchemaGeneratorTest.java | 78 +++++++----------- 27 files changed, 72 insertions(+), 82 deletions(-) create mode 100644 java/SchemaValidation.class create mode 100644 java/TestArrayHolder.class create mode 100644 java/TestBooleanBoxedHolder.class create mode 100644 java/TestBooleanHolder.class create mode 100644 java/TestDateTimeHolder.class create mode 100644 java/TestDoubleHolder.class create mode 100644 java/TestEnumColor.class create mode 100644 java/TestFloatHolder.class create mode 100644 java/TestIntHolder.class create mode 100644 java/TestIntegerHolder.class create mode 100644 java/TestListHolder.class create mode 100644 java/TestLongHolder.class create mode 100644 java/TestMapBoolHolder.class create mode 100644 java/TestMapHolder.class create mode 100644 java/TestMapLongHolder.class create mode 100644 java/TestMapObjectHolder.class create mode 100644 java/TestNestedHolder.class create mode 100644 java/TestObjectHolder.class create mode 100644 java/TestOptionalHolder.class create mode 100644 java/TestParamsHolder.class create mode 100644 java/TestRecordPerson.class create mode 100644 java/TestRecordWithOptional.class create mode 100644 java/TestStringHolder.class create mode 100644 java/TestUuidHolder.class create mode 100644 java/TestValidJavaHolder.class diff --git a/java/SchemaValidation.class b/java/SchemaValidation.class new file mode 100644 index 0000000000000000000000000000000000000000..ac5232aff6c0c193c6b793a2b7ac3e2aaa07775f GIT binary patch literal 597 zcma)3Jx>Bb5Pf@aAAAX(fTE(J4Nyp-HezgyR)8iL5)+#RR$Ox2k>eoYXIV)s`~m(b z;~bzQT;moqZ{N(mmzn+g{`drNi<2l6gmi@CP@yrT`h3DG7Potq`*vS+JO=Gb+S0pb z2o*~$0|rCbn+}D6DAh)uEA3tkaU^tX#F0dbAu$&jd(x`Z`H(?zUKlQlwRP=kkSQ&w z-?vo*CWFddmro6(2}(~4M!sLxH```-`j>9dDaZu9b2OndUxR@?hBWVXr6(PmTMw=? z6s{-5NJpO5*6h5B0dH|jcDdg{OS*Bof+1OxwrGq8ZQ(xhwndCFS#%MMHl4BC5qHw( zGwT|s{=QJaK7EmY6wv(8C?3!)Pnl6x%kN-s1Q-q}`eh(Q@#wGhnVzY{o}0QEnITrA rylHMNZ!=`-1b58c713D2%kWA$JJ--)Ku3;Z84;WzN=^mRR&e|S$J>Ns literal 0 HcmV?d00001 diff --git a/java/TestArrayHolder.class b/java/TestArrayHolder.class new file mode 100644 index 0000000000000000000000000000000000000000..3e71ab52590a6bdfcc57f8484dc442adca8ec149 GIT binary patch literal 288 zcmX^0Z`VEs1_oCKel7+k24;2!79Ivx1~x_pfvm)`ME#t^ymWp4q^#8B5=I6#o6Nk- z5<5l)W)00Sb_Nbc2L6!L;u6QAqQpv%{G623B1Q%l=lqmZpoC9mUaDVdZc=JdNMceB zP(-jeIU_YUF(k1lJr$ybkwH{LGuj7XPjE?5W?s5ABLi1(erZv1s#_*VqX?QEdLRu9 ziVW;P_ksWu10%@AK#~*4lLgX@K$=x+I|JiJAOmO`kOWDC*o+L^3_L&*q*Myb2O7Y* P0ZkpqNM4{A69XRrEa^9m literal 0 HcmV?d00001 diff --git a/java/TestBooleanBoxedHolder.class b/java/TestBooleanBoxedHolder.class new file mode 100644 index 0000000000000000000000000000000000000000..9ed8185326dfba8da8e1d744105fb66a21f067b4 GIT binary patch literal 309 zcmZus%WA?<5IqwgX?<@j($a;y=t4iB;zCheNV{lof0GO`5_2I&@w4oz;KC2+M-}Ia zAeGMI%AbLqg8!ENnaw zo?PB!Bdudr$w==xnCUp+l#n0mfC=?^l<@m@?Q!ZVFP2Gb<1cxwT$P6WM@eWpo%!wP z)%A@~`qbOh=V@g0FaKZc86n!RsQ>@~ literal 0 HcmV?d00001 diff --git a/java/TestBooleanHolder.class b/java/TestBooleanHolder.class new file mode 100644 index 0000000000000000000000000000000000000000..6428abbe7522b96a2b6692507f0fd61a5a9d2a83 GIT binary patch literal 276 zcmX^0Z`VEs1_oCKel7+k24;2!79Ivx1~x_pfvm)`ME#t^ymWp4q^#8B5=I6#o6Nk- z5<5l)W)00Sb_Nbc2EmZj;u5F){G8OpJdgaGl++?d1{UZ1lvG9rexJ;|RKL>Pq|~C2 z#H1Xch;VUoMrv+iNMccXDohjDtSCkXuHgLAqU2P!Oprn`3{&+$j$u$_UOe;F0>zjZ F_yG7OGa&!~ literal 0 HcmV?d00001 diff --git a/java/TestDateTimeHolder.class b/java/TestDateTimeHolder.class new file mode 100644 index 0000000000000000000000000000000000000000..37dd2a86a5cf3b8c4b8d381506dff746b1ecd67a GIT binary patch literal 304 zcmX^0Z`VEs1_oCKel7+k24;2!79Ivx1~x_pfvm)`ME#t^ymWp4q^#8B5=I6#o6Nk- z5<5l)W)00Sb_Nbc2BDDD;u4p{lGKpQ+*FVJoRrieMg|t={FGEi27aH+yi~u^+@#c^ zki?`MpomCuaz<)yVn||9dMaEOBZG{FrVrS%5}+aa{%L8&2w7`J2Cm@z(xT*4w@i=` z;+Pidf%GycGOz>P4+2aKj36HXNlqY77DzJ!X;!W642&Cr44{EP5+n^#%FVz7WP_AS T0co%(;|4T!%s_EouzEfKF+x3f literal 0 HcmV?d00001 diff --git a/java/TestDoubleHolder.class b/java/TestDoubleHolder.class new file mode 100644 index 0000000000000000000000000000000000000000..ddef88d86762adeb0e061dca217e2e9274caa497 GIT binary patch literal 273 zcmZ9G%?`m(5QWdQrPOaEHWCXvEO-I2@e>PSp|QWM36*M-YQ2_NNh~~ohZ1we%3Yk9 zxpU^rdA%P`05jAa7%**EW#o`2)Q{?-hM|h~!?ky0KPBWxK@_B8g4ykDZ4|`h@;RNx znHTaR4tJaotVz6MLTwpDyvj}g&Pd{VAT_WZl~lBNV_6K9Ff^FaHW|IC3- yf(~p6sGkAS5k-qKwWX5OT>lQbNh3H>z1BUUgtDl%w!WtVs9yhk8d;G)1Jw`J88PSp literal 0 HcmV?d00001 diff --git a/java/TestEnumColor.class b/java/TestEnumColor.class new file mode 100644 index 0000000000000000000000000000000000000000..4cd9c6dfa62558184082a5a27910cbc13b86d68a GIT binary patch literal 948 zcmZuvZBNrs6n<`B)~$A~R%C*p@}jImH_`ZM3qfZgCKC+BC5xZBQD;flCSAv8f0B+w zh(^O_eiZTCrpCS4q&+?7p65BwIpDjeaNVa9mCx zsbuMWN=86MSNZlRmCLO{qAyU)`~qu>BZzie8bPt!@`#2st0ha2N($ah!IhtcH6DuR jSUH23IIB|PD2cNsCysi7^)uvWbqcApXV@Ys0-pZ?U#zkA literal 0 HcmV?d00001 diff --git a/java/TestFloatHolder.class b/java/TestFloatHolder.class new file mode 100644 index 0000000000000000000000000000000000000000..18d7130ff77499235c562f416db1457567ae85a8 GIT binary patch literal 270 zcmZ9G%MQU%6h-$bRgZclMiK)v4D<)YL^KA%Kx2MeCse9U+VWWvBZ+|z@KNGiX6|60 z*S;(J^?p17%+ajEK*2)MhKUlPbyOGSL@M4pYyZeWPAE;nILxPn!eF?yP$o1z&T=|)@5g)@d3bfFtY#v literal 0 HcmV?d00001 diff --git a/java/TestIntHolder.class b/java/TestIntHolder.class new file mode 100644 index 0000000000000000000000000000000000000000..0781a3c1f65309d2f86ed31c6e9b0f92fd7f0db3 GIT binary patch literal 264 zcmZ9Gy$%6U5QWd|V*L}PM4>~$3y4MtiBQ<+@2&|eYd2ZTQ)yHZg$M9ZVn)X;&ivdn zU(W0OcmkN9R)huHL8go>a)kQ9T+ARe(QdHv4(z9d+%SlObVRT_-Hn4hq2lr>ok!^` z4!4{TGUIs5gxVsAczHg0oVdmdb#wCfd^D~}cB}_q#+*=G$LGZ7X`rUY53R2O(1s(q t^jRQHCR&iGC6%ORdw0-H8exg*xAue*%A#s*erEyL*FT>kE3>{Q#g|E=2$U literal 0 HcmV?d00001 diff --git a/java/TestIntegerHolder.class b/java/TestIntegerHolder.class new file mode 100644 index 0000000000000000000000000000000000000000..e3317aca77f71836b6fa626a271e061e6bda7bbe GIT binary patch literal 294 zcmZWj%Syvg5IvJeqcIvq+J)f4UFyPofYMD-6pAhk-QOfbOh^;BN&POnDs<5g=tqfj zMG@*O&b-b!GrP~t7Qj719~Nv6P5>7J!fB=6Ra~p4h+oo*vz{=xE1R;P5bS6?_i#Ws zOStQwnw|^Rk8Pc^COFeJXF@nD8-89cQr3w|YnhyP*&8oZqIAK(BEm2l&;CyRc-<2G z*LJBhekhG}^zY7%(clW6d~L!4nZE@OrMr|&lHKbyXeEXC37WO32}cM74fW!?7X$j( M;~C3inOQjb0*vN4`~Uy| literal 0 HcmV?d00001 diff --git a/java/TestListHolder.class b/java/TestListHolder.class new file mode 100644 index 0000000000000000000000000000000000000000..42efc5127a208284e8f9923a338fd450a7d330c7 GIT binary patch literal 343 zcmZXPu};G<5QhIt+J=_W2H21oSWwGA9-vhh1Y(F_NW|`>R&bR%lH>4PFd;GY0eC3H z*<}Gd+@0<3zyI#@>thGt7RMt52qW~y=wm>LH{wMkwa``aSZriz34^85%B~3EZ2lDC zfH28rYg5(Q`=(w?L+IT!Ye|@GbzZ*UkbLhK*jP1vlg9zW3-M#w>B@*$*1f>mM$}k*6n2Onnu914n{JnE(I) literal 0 HcmV?d00001 diff --git a/java/TestLongHolder.class b/java/TestLongHolder.class new file mode 100644 index 0000000000000000000000000000000000000000..6fa67a8691d332f4b4d63841c6fdef8f2e24297e GIT binary patch literal 267 zcmZ9G%?`m(5QWdwPt{)~b`lFaEO-I25h5ZiH1@acq$<@WRo=?NN@C#wJd~K*9d~i& z=brg;-k;|Sz#MiJ1_~yMb(Byhw2tbk#*vDH@zy=Eml4X7Fb=aRq0k@fO;iX?htJtM ziGyVl`J57pv&3hDy$)mETuv^hj&dW}w9ejvPs&kg!1+Z^SrMwcZ6SgVYn2}BG$r`@qNCOg(GyP$|yG@gyYFW zgdM^tlWTV?o-V3N-&A@bEunQ;EhJ$uS4Q5|t6bVl*7(aL?&!0f16lhJefD6 zYNvF}S_z?>s?rEoTkbmFu1`03vsvn_GG+27<2RAQBr}%jrB!VUd9D2P!+)!u`LQ@b z#E1JAfc)ufyZk$5Pwd;LZ=hEWSdXnQZ_r)zxy7SLKWIQNTU>~}hTdml&_u&;$y`s$ literal 0 HcmV?d00001 diff --git a/java/TestMapHolder.class b/java/TestMapHolder.class new file mode 100644 index 0000000000000000000000000000000000000000..8b84b1076f34ed5dd92d05fd5bbced680ddcdcc4 GIT binary patch literal 356 zcmZXPzf!_L5XQevfCz$sr85?GU|~uN7#c^%0<}oS{t|ZLG2!AI7oSTjXDoaGAIfnr z*oar`?(P1*{qyzx@d@A>;~oNp5n6q;(IJf1>P5w+GDZB5tvPpu&RiSq7KCszeT;BG z7^J*)ck21JDpza?t*dIqgyB*fzOOeK+f-$x&usH2-l$aBg1vw+o=lg`h}!8gmeh>U zO>|+DtF0(5_v`Z=-fWaOt4%TcpIJzkUQ*RI=Ns(@AOG3#!cW8*BDvMS0OYR~+m+v` ocoN?}e+RuuAic!;@(w*jUnCwq`9%YI-Qz+G8v00xVH1sh0E3oF&;S4c literal 0 HcmV?d00001 diff --git a/java/TestMapLongHolder.class b/java/TestMapLongHolder.class new file mode 100644 index 0000000000000000000000000000000000000000..cf74116bd2eb747101c3b7625d698c3123c580e1 GIT binary patch literal 366 zcmZXPu};H442FLvZ9_|;t&kXy7}#0{GBKs5UeNWzbI}Qjfd}BB zP<<&#sN7)t?9adLukVjf0Jj+R;K2{j>Z6SgVYn2pA}ob2!pCeWb4%z-<@+L@G=nf16lhJc*l8 zwN+)vS`nd}s6q={8}7Q^uTOV)vsq$|(na(q<2H$AtkFxV+T?Ps-00JPdtSMjxIn;H xyXS%2-E6!3J7-Vq+n4X44F{~p)|GeYA^P0n(6b-ZqqjZI$5BHcGcjnQ;TB$7PNM(- literal 0 HcmV?d00001 diff --git a/java/TestMapObjectHolder.class b/java/TestMapObjectHolder.class new file mode 100644 index 0000000000000000000000000000000000000000..c2e426968b4142212a10067e1e07a0bb64bb0e26 GIT binary patch literal 374 zcmZvXzfQw25XQet+J=@wTOlNb#K6`vkdYx(mkPvCU`WL7q*id1I>>c+E|`!QcmN)% z;+!CXDm>hs?|$F?`PzPb0=UMg2M>OLRv&G22*Z_l5n&;89zLWinOQ<-s#ULTg?wC1J2oTHaUdRGLJj1!s;o*^^w0M3`Lu$_S(JWKoZ+ ztSUn87ZJL#%C)eSVcpekecIslcCj@|=h2@`vrRPPF1@&{OeSxX8-Mc8wwG=$&JpnS x?s*`0Kie+<&e#+C_QgBsjRWq;)|DG{AAOcM^z;Yy=yiwlaZuBTObqI1_zhWlQO5uP literal 0 HcmV?d00001 diff --git a/java/TestNestedHolder.class b/java/TestNestedHolder.class new file mode 100644 index 0000000000000000000000000000000000000000..d1f9e0b71ac1466dc2648a7bab54ec8f3e03653f GIT binary patch literal 386 zcmZutu};G<5Pg@V4K0PX0}B!ZTgyN`AXOFw0x6Us5xbLE!Bv_d$KktRLSoH$>90S{pcr z&p%<01G0a?IO6JzBXJyDyo262;dP9*4Y-VlnDB^|o^oYbhh7o7q2XTiL5ZOJ4#)4zjmQiz?PUhA4rL`hInTi;#`=$hbZ%d*T2R6fqGI2-@~ literal 0 HcmV?d00001 diff --git a/java/TestOptionalHolder.class b/java/TestOptionalHolder.class new file mode 100644 index 0000000000000000000000000000000000000000..07deabf203162d3dc308af33f0d68b942d11add0 GIT binary patch literal 363 zcmZvXu};G<5QhIt+J=@w3ql|vF|f5A$OE+MQh^vMIwWFuQY*M}9ppMZ7feVDJOB@c zdQO1>@NjpwzyJQbkI%Pv0M|I`AV3(Q(M1z2LVqQmM687=;zhQSxg)eD$|yG_goELI zggwG>DmQNN=u~NhzAg1qT0&!1E+wHiS4Q4d>rC2IWSU=2Hu-~Gi&WS`{-_A2gW`wsl uOLJ`V?SeCLZjN3-FI;d#j{bXx?xV{RkDh;_0X_fXLLAieArrm2>Tdx!x=#N9 literal 0 HcmV?d00001 diff --git a/java/TestParamsHolder.class b/java/TestParamsHolder.class new file mode 100644 index 0000000000000000000000000000000000000000..780408b676c6a1d4467334ea1fa9eac28cd8a26d GIT binary patch literal 290 zcmZ8b%WA?<5Iqy4X{}W^rF7ASyXZncpv8?;EJ#~0xajW24B<*l@A9L=OKWj=XNx4iL$*0U|GpAs7Ja2?@F zND0^e!#K067gdq5BZRXmV?u9fOMW}-Qg(?+3+eRt%45&2PK?W01n=YJ|CN<@w#=Vi zKZk2VdsQ9WhUZo{?=G*5bqfPT^0Hb0(x(NlrFxV|5}TuM(3d14C#Yl12{-5nD)m7; O)b1rs6>nt_p!)|SKRCPq literal 0 HcmV?d00001 diff --git a/java/TestRecordPerson.class b/java/TestRecordPerson.class new file mode 100644 index 0000000000000000000000000000000000000000..0e1585005aa3ee1f7638b279c0ad1092b32c74e1 GIT binary patch literal 1353 zcmaJ=T~pIg5IvWcrqtAaSVR;kDj#h?>i1VsXG9%4Q2r=QhCx+TkJDo6Xs?XLs-Z`Frvkz+1dZVFWP^aUG*b2xRxnL$lI0?VZYJ z*>v2NKw{OhE&ruJtW@3_LlR>eQaUa{Cwxm zFYCx4OD=PVAW>7kypAge6>3 r$`Ke?qAhR&LH~p92fbhBpDgL_a{@`+K?Qd?v1jzA=zWe=yu{=`%JLp% literal 0 HcmV?d00001 diff --git a/java/TestRecordWithOptional.class b/java/TestRecordWithOptional.class new file mode 100644 index 0000000000000000000000000000000000000000..70cee73fe486e65acb3729026469b31ca8c2726d GIT binary patch literal 1543 zcma)6T~8B16g^XDx2;=RDpavTtAe)6N5yYl;6c<#QpE_tM4z_pgf1+*b$6D;|Kx)t z5)&T$0sbiCo$fYl(}no3Gn2dLo_o)|cjnLEv)=$-Vg zw+2I7cH+?#JY&Y|DaM_V4TgpRx4di)jl+OF9XjvIB^ zv<%OS8p84TGD;GSK6EL<<}j#Rw$0r>CHEA(U|2ZUe2E4HYYcOjL#YpeV64(*Pta{j zpeP>ZIz{e;OboYZm2gRSo7PN% zI0;K%z;*|d^j6Fhq)N65?GmhLXJp3@Iu3J8(xb2t1D*!H$AHVY8-lBpfK-*%7SAvf zhh~)7Adgw{l6fSxpHN1#U?f1wt`Z~)&5c3>l{Ji`PT&MIEYe=0Q&8LC$}Z8r35vW* V_%xQ$zq25Ixl%J&Zwwjbs*fSnvVFMu>&5(3$=1R_VFYx|6H;o?A21&!>JEuS zEl$18IaPnR%N4)`l_DHuTx3hgAy24o)Is$l6^H$$zu_Py4=bNcYa)a2+8AJ zx=M|X!&wro*$}ehWX*)iT*tiFZ+$kN@*|nlcEK-im8VR|PYt2o?#y3@9!>*7ah2>% zz*B97jrSuxt3V5`d~Cx3*`Eaq(tS%N$+_+sbdo~s1nt_^gin+NEw%aIivb7x#426#g5X!$`tJHxFI*d*)1Lm_;_ z1Ri{Rk3akR|ML3>;2oVhO0aB{o2Z~l=^kP)g=9qVjHu!f^g8#O}P ze_w+g;}1-HR8(f|Me literal 0 HcmV?d00001 diff --git a/java/TestValidJavaHolder.class b/java/TestValidJavaHolder.class new file mode 100644 index 0000000000000000000000000000000000000000..d8089799f2e5cba0c6da7d02130434d648cc8d15 GIT binary patch literal 720 zcmaix%}&BV6ot>oPXSSWMKp2Y#sCXhx{$Cj8e>ov5cUI1>X6cs()wJwGI8Mp_)x|( zh!LcbF7C8vzI)E}&d2B5JAi8(Ww3*og?JVTBn9#l`6Qc;bnRw$I8mcOAlcHc4%z~- zT76(4B~a?Cc`%TU9^a9^^_;Qt1>#rUSPA4h+EowXbg2Bk96Cmsj~>-j_N8yDpcfD= z*Xo_$AWGV`FX_7qcJ%xkF8;t_pq-|XiKctnc4ZLy^gr7GTAS`|HWgR>@8xW9mcm*$ zRe~3PwFtxLh_!$1&N#lxninPQxzb@sBXT1m$s9e?^Fn{5ZnPO_HDdnfW}Y~O#rrf* z0>a!VYnoptT!m2^uMjVMp(Cr=2I8!{$g&#rn5$8D#53zEXDs)y&w&5AByy%7dGZAm XxANAKFOe_fU@MThis class is invoked by the annotation processor and operates exclusively - * with the {@code javax.lang.model} API. It does NOT use {@code java.lang.reflect}. + *

+ * This class is invoked by the annotation processor and operates exclusively + * with the {@code javax.lang.model} API. It does NOT use + * {@code java.lang.reflect}. * * @since 1.0.2 */ @@ -33,12 +35,15 @@ public class SchemaGenerator { /** * Given a {@link TypeMirror} from the annotation processing environment, - * returns a {@code String} containing Java source code for a {@code Map} literal - * representing the JSON Schema of that type. + * returns a {@code String} containing Java source code for a {@code Map} + * literal representing the JSON Schema of that type. * - * @param type the type to generate schema for - * @param typeUtils the {@link Types} utility from the processing environment - * @param elementUtils the {@link Elements} utility from the processing environment + * @param type + * the type to generate schema for + * @param typeUtils + * the {@link Types} utility from the processing environment + * @param elementUtils + * the {@link Elements} utility from the processing environment * @return a Java source code string representing the JSON Schema */ public String generateSchemaSource(TypeMirror type, Types typeUtils, Elements elementUtils) { @@ -47,15 +52,19 @@ public String generateSchemaSource(TypeMirror type, Types typeUtils, Elements el /** * Generates the full "parameters" schema source for a method's parameters. - * Produces a {@code Map.of("type", "object", "properties", Map.of(...), "required", List.of(...))}. + * Produces a + * {@code Map.of("type", "object", "properties", Map.of(...), "required", List.of(...))}. * - * @param parameters the method parameters to generate schema for - * @param typeUtils the {@link Types} utility from the processing environment - * @param elementUtils the {@link Elements} utility from the processing environment + * @param parameters + * the method parameters to generate schema for + * @param typeUtils + * the {@link Types} utility from the processing environment + * @param elementUtils + * the {@link Elements} utility from the processing environment * @return a Java source code string representing the parameters JSON Schema */ - public String generateParametersSchemaSource( - List parameters, Types typeUtils, Elements elementUtils) { + public String generateParametersSchemaSource(List parameters, Types typeUtils, + Elements elementUtils) { if (parameters.isEmpty()) { return "Map.of(\"type\", \"object\", \"properties\", Map.of(), \"required\", List.of())"; } @@ -116,19 +125,19 @@ private String generateSchema(TypeMirror type, Types typeUtils, Elements element private String generatePrimitiveSchema(TypeKind kind) { switch (kind) { - case INT: - case LONG: - case BYTE: - case SHORT: + case INT : + case LONG : + case BYTE : + case SHORT : return "Map.of(\"type\", \"integer\")"; - case DOUBLE: - case FLOAT: + case DOUBLE : + case FLOAT : return "Map.of(\"type\", \"number\")"; - case BOOLEAN: + case BOOLEAN : return "Map.of(\"type\", \"boolean\")"; - case CHAR: + case CHAR : return "Map.of(\"type\", \"string\")"; - default: + default : return "Map.of()"; } } @@ -220,8 +229,7 @@ private String generateDeclaredTypeSchema(DeclaredType type, Types typeUtils, El if (typeElement.getKind() == ElementKind.ENUM) { List constants = typeElement.getEnclosedElements().stream() .filter(e -> e.getKind() == ElementKind.ENUM_CONSTANT) - .map(e -> "\"" + e.getSimpleName().toString() + "\"") - .collect(Collectors.toList()); + .map(e -> "\"" + e.getSimpleName().toString() + "\"").collect(Collectors.toList()); return "Map.of(\"type\", \"string\", \"enum\", List.of(" + String.join(", ", constants) + "))"; } @@ -256,8 +264,8 @@ private String generateRecordSchema(TypeElement typeElement, Types typeUtils, El boolean isOptional = isOptionalType(componentType, typeUtils, elementUtils); String schema; if (isOptional) { - schema = generateSchema( - unwrapOptional(componentType, typeUtils, elementUtils), typeUtils, elementUtils); + schema = generateSchema(unwrapOptional(componentType, typeUtils, elementUtils), typeUtils, + elementUtils); } else { schema = generateSchema(componentType, typeUtils, elementUtils); requiredNames.add("\"" + name + "\""); @@ -290,8 +298,8 @@ private String generateClassSchema(TypeElement typeElement, Types typeUtils, Ele boolean isOptional = isOptionalType(fieldType, typeUtils, elementUtils); String schema; if (isOptional) { - schema = generateSchema( - unwrapOptional(fieldType, typeUtils, elementUtils), typeUtils, elementUtils); + schema = generateSchema(unwrapOptional(fieldType, typeUtils, elementUtils), typeUtils, + elementUtils); } else { schema = generateSchema(fieldType, typeUtils, elementUtils); requiredNames.add("\"" + name + "\""); @@ -314,8 +322,7 @@ private String generateClassSchema(TypeElement typeElement, Types typeUtils, Ele private String generateSealedSchema(TypeElement typeElement, Types typeUtils, Elements elementUtils) { List permittedSubclasses = typeElement.getPermittedSubclasses(); if (permittedSubclasses != null && !permittedSubclasses.isEmpty()) { - List schemas = permittedSubclasses.stream() - .map(sub -> generateSchema(sub, typeUtils, elementUtils)) + List schemas = permittedSubclasses.stream().map(sub -> generateSchema(sub, typeUtils, elementUtils)) .collect(Collectors.toList()); return "Map.of(\"oneOf\", List.of(" + String.join(", ", schemas) + "))"; } @@ -329,10 +336,8 @@ private boolean isOptionalType(TypeMirror type, Types typeUtils, Elements elemen DeclaredType declaredType = (DeclaredType) type; TypeElement element = (TypeElement) declaredType.asElement(); String name = element.getQualifiedName().toString(); - return "java.util.Optional".equals(name) - || "java.util.OptionalInt".equals(name) - || "java.util.OptionalDouble".equals(name) - || "java.util.OptionalLong".equals(name); + return "java.util.Optional".equals(name) || "java.util.OptionalInt".equals(name) + || "java.util.OptionalDouble".equals(name) || "java.util.OptionalLong".equals(name); } private TypeMirror unwrapOptional(TypeMirror type, Types typeUtils, Elements elementUtils) { @@ -362,8 +367,7 @@ private TypeMirror unwrapOptional(TypeMirror type, Types typeUtils, Elements ele } private boolean isCollectionType(String qualifiedName) { - return "java.util.List".equals(qualifiedName) - || "java.util.Collection".equals(qualifiedName) + return "java.util.List".equals(qualifiedName) || "java.util.Collection".equals(qualifiedName) || "java.util.Set".equals(qualifiedName); } diff --git a/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java index 292ba87cf..bfbe94314 100644 --- a/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java +++ b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java @@ -36,9 +36,9 @@ import org.junit.jupiter.api.Test; /** - * Tests for {@link SchemaGenerator} using the compilation-testing approach. - * A test annotation processor exercises SchemaGenerator during compilation - * of small source snippets. + * Tests for {@link SchemaGenerator} using the compilation-testing approach. A + * test annotation processor exercises SchemaGenerator during compilation of + * small source snippets. */ public class SchemaGeneratorTest { @@ -90,7 +90,8 @@ public boolean process(Set annotations, RoundEnvironment for (Element rootElement : roundEnv.getRootElements()) { if (rootElement.getKind() == ElementKind.CLASS || rootElement.getKind() == ElementKind.RECORD - || rootElement.getKind() == ElementKind.INTERFACE || rootElement.getKind() == ElementKind.ENUM) { + || rootElement.getKind() == ElementKind.INTERFACE + || rootElement.getKind() == ElementKind.ENUM) { // Find methods named "schemaTarget" to capture schemas for their return type for (Element enclosed : rootElement.getEnclosedElements()) { if (enclosed.getKind() == ElementKind.METHOD) { @@ -103,8 +104,8 @@ public boolean process(Set annotations, RoundEnvironment } if ("parametersTarget".equals(methodName)) { List params = method.getParameters(); - String schema = - generator.generateParametersSchemaSource(params, typeUtils, elementUtils); + String schema = generator.generateParametersSchemaSource(params, typeUtils, + elementUtils); capturedParameterSchemas.add(schema); } } @@ -115,8 +116,7 @@ public boolean process(Set annotations, RoundEnvironment String typeName = typeElement.getSimpleName().toString(); if (typeName.startsWith("TestRecord") || typeName.startsWith("TestEnum") || typeName.startsWith("TestSealed")) { - String schema = - generator.generateSchemaSource(typeElement.asType(), typeUtils, elementUtils); + String schema = generator.generateSchemaSource(typeElement.asType(), typeUtils, elementUtils); capturedSchemas.add(typeName + "=" + schema); } } @@ -147,8 +147,7 @@ private List compileAndCapture(List sourceTexts) { } // Compile with the processor on classpath - JavaCompiler.CompilationTask task = compiler.getTask( - null, // writer + JavaCompiler.CompilationTask task = compiler.getTask(null, // writer null, // file manager diagnostics, // diagnostics List.of("--add-modules", "ALL-MODULE-PATH"), // options @@ -192,7 +191,7 @@ private List compileAndCaptureParams(String source) { private String extractClassName(String source) { // Simple extraction: find "class X", "record X", "enum X", or "interface X" - for (String keyword : new String[] {"class ", "record ", "enum ", "interface "}) { + for (String keyword : new String[]{"class ", "record ", "enum ", "interface "}) { int idx = source.indexOf(keyword); if (idx >= 0) { int start = idx + keyword.length(); @@ -304,8 +303,8 @@ public class TestArrayHolder { } """; List schemas = compileAndCapture(source); - assertContainsSchema( - schemas, "schemaTargetArray", "Map.of(\"type\", \"array\", \"items\", Map.of(\"type\", \"string\"))"); + assertContainsSchema(schemas, "schemaTargetArray", + "Map.of(\"type\", \"array\", \"items\", Map.of(\"type\", \"string\"))"); } @Test @@ -314,9 +313,7 @@ void enumType() { public enum TestEnumColor { RED, GREEN, BLUE } """; List schemas = compileAndCapture(source); - assertContainsSchema( - schemas, - "TestEnumColor", + assertContainsSchema(schemas, "TestEnumColor", "Map.of(\"type\", \"string\", \"enum\", List.of(\"RED\", \"GREEN\", \"BLUE\"))"); } @@ -329,8 +326,8 @@ public class TestListHolder { } """; List schemas = compileAndCapture(source); - assertContainsSchema( - schemas, "schemaTargetList", "Map.of(\"type\", \"array\", \"items\", Map.of(\"type\", \"string\"))"); + assertContainsSchema(schemas, "schemaTargetList", + "Map.of(\"type\", \"array\", \"items\", Map.of(\"type\", \"string\"))"); } @Test @@ -342,9 +339,7 @@ public class TestMapHolder { } """; List schemas = compileAndCapture(source); - assertContainsSchema( - schemas, - "schemaTargetMap", + assertContainsSchema(schemas, "schemaTargetMap", "Map.of(\"type\", \"object\", \"additionalProperties\", Map.of(\"type\", \"string\"))"); } @@ -369,9 +364,7 @@ public class TestMapBoolHolder { } """; List schemas = compileAndCapture(source); - assertContainsSchema( - schemas, - "schemaTargetMapBool", + assertContainsSchema(schemas, "schemaTargetMapBool", "Map.of(\"type\", \"object\", \"additionalProperties\", Map.of(\"type\", \"boolean\"))"); } @@ -384,9 +377,7 @@ public class TestMapLongHolder { } """; List schemas = compileAndCapture(source); - assertContainsSchema( - schemas, - "schemaTargetMapLong", + assertContainsSchema(schemas, "schemaTargetMapLong", "Map.of(\"type\", \"object\", \"additionalProperties\", Map.of(\"type\", \"integer\"))"); } @@ -423,8 +414,8 @@ public class TestDateTimeHolder { } """; List schemas = compileAndCapture(source); - assertContainsSchema( - schemas, "schemaTargetDateTime", "Map.of(\"type\", \"string\", \"format\", \"date-time\")"); + assertContainsSchema(schemas, "schemaTargetDateTime", + "Map.of(\"type\", \"string\", \"format\", \"date-time\")"); } @Test @@ -434,8 +425,7 @@ public record TestRecordPerson(String name, int age, boolean active) {} """; List schemas = compileAndCapture(source); String expected = "Map.of(\"type\", \"object\", \"properties\", " - + "Map.of(\"name\", Map.of(\"type\", \"string\"), " - + "\"age\", Map.of(\"type\", \"integer\"), " + + "Map.of(\"name\", Map.of(\"type\", \"string\"), " + "\"age\", Map.of(\"type\", \"integer\"), " + "\"active\", Map.of(\"type\", \"boolean\")), " + "\"required\", List.of(\"name\", \"age\", \"active\"))"; assertContainsSchema(schemas, "TestRecordPerson", expected); @@ -449,8 +439,7 @@ public record TestRecordWithOptional(String name, Optional nickname) {} """; List schemas = compileAndCapture(source); String expected = "Map.of(\"type\", \"object\", \"properties\", " - + "Map.of(\"name\", Map.of(\"type\", \"string\"), " - + "\"nickname\", Map.of(\"type\", \"string\")), " + + "Map.of(\"name\", Map.of(\"type\", \"string\"), " + "\"nickname\", Map.of(\"type\", \"string\")), " + "\"required\", List.of(\"name\"))"; assertContainsSchema(schemas, "TestRecordWithOptional", expected); } @@ -467,17 +456,17 @@ public void parametersTarget(String query, int limit, boolean verbose) {} String schema = paramSchemas.get(0); assertTrue(schema.contains("\"type\", \"object\""), "Should be object type: " + schema); assertTrue(schema.contains("\"query\", Map.of(\"type\", \"string\")"), "Should have query property: " + schema); - assertTrue( - schema.contains("\"limit\", Map.of(\"type\", \"integer\")"), "Should have limit property: " + schema); - assertTrue( - schema.contains("\"verbose\", Map.of(\"type\", \"boolean\")"), + assertTrue(schema.contains("\"limit\", Map.of(\"type\", \"integer\")"), + "Should have limit property: " + schema); + assertTrue(schema.contains("\"verbose\", Map.of(\"type\", \"boolean\")"), "Should have verbose property: " + schema); assertTrue(schema.contains("\"required\", List.of("), "Should have required list: " + schema); } @Test void generatedSourceIsValidJava() { - // Verify that generated schema source code compiles when embedded in a method body + // Verify that generated schema source code compiles when embedded in a method + // body String source = """ import java.util.List; import java.util.Map; @@ -510,16 +499,14 @@ public class TestValidJavaHolder { // Compile the validation source to verify syntactic validity JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); DiagnosticCollector diagnostics = new DiagnosticCollector<>(); - List compilationUnits = - List.of(new InMemorySource("SchemaValidation", validationSource.toString())); + List compilationUnits = List + .of(new InMemorySource("SchemaValidation", validationSource.toString())); JavaCompiler.CompilationTask task = compiler.getTask(null, null, diagnostics, null, null, compilationUnits); boolean success = task.call(); - assertTrue( - success, - "Generated schema source code is not valid Java: " - + diagnostics.getDiagnostics() + "\nSource:\n" + validationSource); + assertTrue(success, "Generated schema source code is not valid Java: " + diagnostics.getDiagnostics() + + "\nSource:\n" + validationSource); } @Test @@ -550,8 +537,7 @@ public class TestObjectHolder { private void assertContainsSchema(List schemas, String methodName, String expectedSchema) { String expected = methodName + "=" + expectedSchema; - assertTrue( - schemas.stream().anyMatch(s -> s.equals(expected)), + assertTrue(schemas.stream().anyMatch(s -> s.equals(expected)), "Expected schema '" + expected + "' not found in: " + schemas); } } From e608bd36031e4c84d43f512850eb411e0ea35f15 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Tue, 23 Jun 2026 13:12:28 -0400 Subject: [PATCH 06/13] Remove .class files generated by test --- java/SchemaValidation.class | Bin 597 -> 0 bytes java/TestArrayHolder.class | Bin 288 -> 0 bytes java/TestBooleanBoxedHolder.class | Bin 309 -> 0 bytes java/TestBooleanHolder.class | Bin 276 -> 0 bytes java/TestDateTimeHolder.class | Bin 304 -> 0 bytes java/TestDoubleHolder.class | Bin 273 -> 0 bytes java/TestEnumColor.class | Bin 948 -> 0 bytes java/TestFloatHolder.class | Bin 270 -> 0 bytes java/TestIntHolder.class | Bin 264 -> 0 bytes java/TestIntegerHolder.class | Bin 294 -> 0 bytes java/TestListHolder.class | Bin 343 -> 0 bytes java/TestLongHolder.class | Bin 267 -> 0 bytes java/TestMapBoolHolder.class | Bin 369 -> 0 bytes java/TestMapHolder.class | Bin 356 -> 0 bytes java/TestMapLongHolder.class | Bin 366 -> 0 bytes java/TestMapObjectHolder.class | Bin 374 -> 0 bytes java/TestNestedHolder.class | Bin 386 -> 0 bytes java/TestObjectHolder.class | Bin 290 -> 0 bytes java/TestOptionalHolder.class | Bin 363 -> 0 bytes java/TestParamsHolder.class | Bin 290 -> 0 bytes java/TestRecordPerson.class | Bin 1353 -> 0 bytes java/TestRecordWithOptional.class | Bin 1543 -> 0 bytes java/TestStringHolder.class | Bin 290 -> 0 bytes java/TestUuidHolder.class | Bin 282 -> 0 bytes java/TestValidJavaHolder.class | Bin 720 -> 0 bytes 25 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 java/SchemaValidation.class delete mode 100644 java/TestArrayHolder.class delete mode 100644 java/TestBooleanBoxedHolder.class delete mode 100644 java/TestBooleanHolder.class delete mode 100644 java/TestDateTimeHolder.class delete mode 100644 java/TestDoubleHolder.class delete mode 100644 java/TestEnumColor.class delete mode 100644 java/TestFloatHolder.class delete mode 100644 java/TestIntHolder.class delete mode 100644 java/TestIntegerHolder.class delete mode 100644 java/TestListHolder.class delete mode 100644 java/TestLongHolder.class delete mode 100644 java/TestMapBoolHolder.class delete mode 100644 java/TestMapHolder.class delete mode 100644 java/TestMapLongHolder.class delete mode 100644 java/TestMapObjectHolder.class delete mode 100644 java/TestNestedHolder.class delete mode 100644 java/TestObjectHolder.class delete mode 100644 java/TestOptionalHolder.class delete mode 100644 java/TestParamsHolder.class delete mode 100644 java/TestRecordPerson.class delete mode 100644 java/TestRecordWithOptional.class delete mode 100644 java/TestStringHolder.class delete mode 100644 java/TestUuidHolder.class delete mode 100644 java/TestValidJavaHolder.class diff --git a/java/SchemaValidation.class b/java/SchemaValidation.class deleted file mode 100644 index ac5232aff6c0c193c6b793a2b7ac3e2aaa07775f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 597 zcma)3Jx>Bb5Pf@aAAAX(fTE(J4Nyp-HezgyR)8iL5)+#RR$Ox2k>eoYXIV)s`~m(b z;~bzQT;moqZ{N(mmzn+g{`drNi<2l6gmi@CP@yrT`h3DG7Potq`*vS+JO=Gb+S0pb z2o*~$0|rCbn+}D6DAh)uEA3tkaU^tX#F0dbAu$&jd(x`Z`H(?zUKlQlwRP=kkSQ&w z-?vo*CWFddmro6(2}(~4M!sLxH```-`j>9dDaZu9b2OndUxR@?hBWVXr6(PmTMw=? z6s{-5NJpO5*6h5B0dH|jcDdg{OS*Bof+1OxwrGq8ZQ(xhwndCFS#%MMHl4BC5qHw( zGwT|s{=QJaK7EmY6wv(8C?3!)Pnl6x%kN-s1Q-q}`eh(Q@#wGhnVzY{o}0QEnITrA rylHMNZ!=`-1b58c713D2%kWA$JJ--)Ku3;Z84;WzN=^mRR&e|S$J>Ns diff --git a/java/TestArrayHolder.class b/java/TestArrayHolder.class deleted file mode 100644 index 3e71ab52590a6bdfcc57f8484dc442adca8ec149..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 288 zcmX^0Z`VEs1_oCKel7+k24;2!79Ivx1~x_pfvm)`ME#t^ymWp4q^#8B5=I6#o6Nk- z5<5l)W)00Sb_Nbc2L6!L;u6QAqQpv%{G623B1Q%l=lqmZpoC9mUaDVdZc=JdNMceB zP(-jeIU_YUF(k1lJr$ybkwH{LGuj7XPjE?5W?s5ABLi1(erZv1s#_*VqX?QEdLRu9 ziVW;P_ksWu10%@AK#~*4lLgX@K$=x+I|JiJAOmO`kOWDC*o+L^3_L&*q*Myb2O7Y* P0ZkpqNM4{A69XRrEa^9m diff --git a/java/TestBooleanBoxedHolder.class b/java/TestBooleanBoxedHolder.class deleted file mode 100644 index 9ed8185326dfba8da8e1d744105fb66a21f067b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 309 zcmZus%WA?<5IqwgX?<@j($a;y=t4iB;zCheNV{lof0GO`5_2I&@w4oz;KC2+M-}Ia zAeGMI%AbLqg8!ENnaw zo?PB!Bdudr$w==xnCUp+l#n0mfC=?^l<@m@?Q!ZVFP2Gb<1cxwT$P6WM@eWpo%!wP z)%A@~`qbOh=V@g0FaKZc86n!RsQ>@~ diff --git a/java/TestBooleanHolder.class b/java/TestBooleanHolder.class deleted file mode 100644 index 6428abbe7522b96a2b6692507f0fd61a5a9d2a83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 276 zcmX^0Z`VEs1_oCKel7+k24;2!79Ivx1~x_pfvm)`ME#t^ymWp4q^#8B5=I6#o6Nk- z5<5l)W)00Sb_Nbc2EmZj;u5F){G8OpJdgaGl++?d1{UZ1lvG9rexJ;|RKL>Pq|~C2 z#H1Xch;VUoMrv+iNMccXDohjDtSCkXuHgLAqU2P!Oprn`3{&+$j$u$_UOe;F0>zjZ F_yG7OGa&!~ diff --git a/java/TestDateTimeHolder.class b/java/TestDateTimeHolder.class deleted file mode 100644 index 37dd2a86a5cf3b8c4b8d381506dff746b1ecd67a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 304 zcmX^0Z`VEs1_oCKel7+k24;2!79Ivx1~x_pfvm)`ME#t^ymWp4q^#8B5=I6#o6Nk- z5<5l)W)00Sb_Nbc2BDDD;u4p{lGKpQ+*FVJoRrieMg|t={FGEi27aH+yi~u^+@#c^ zki?`MpomCuaz<)yVn||9dMaEOBZG{FrVrS%5}+aa{%L8&2w7`J2Cm@z(xT*4w@i=` z;+Pidf%GycGOz>P4+2aKj36HXNlqY77DzJ!X;!W642&Cr44{EP5+n^#%FVz7WP_AS T0co%(;|4T!%s_EouzEfKF+x3f diff --git a/java/TestDoubleHolder.class b/java/TestDoubleHolder.class deleted file mode 100644 index ddef88d86762adeb0e061dca217e2e9274caa497..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 273 zcmZ9G%?`m(5QWdQrPOaEHWCXvEO-I2@e>PSp|QWM36*M-YQ2_NNh~~ohZ1we%3Yk9 zxpU^rdA%P`05jAa7%**EW#o`2)Q{?-hM|h~!?ky0KPBWxK@_B8g4ykDZ4|`h@;RNx znHTaR4tJaotVz6MLTwpDyvj}g&Pd{VAT_WZl~lBNV_6K9Ff^FaHW|IC3- yf(~p6sGkAS5k-qKwWX5OT>lQbNh3H>z1BUUgtDl%w!WtVs9yhk8d;G)1Jw`J88PSp diff --git a/java/TestEnumColor.class b/java/TestEnumColor.class deleted file mode 100644 index 4cd9c6dfa62558184082a5a27910cbc13b86d68a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 948 zcmZuvZBNrs6n<`B)~$A~R%C*p@}jImH_`ZM3qfZgCKC+BC5xZBQD;flCSAv8f0B+w zh(^O_eiZTCrpCS4q&+?7p65BwIpDjeaNVa9mCx zsbuMWN=86MSNZlRmCLO{qAyU)`~qu>BZzie8bPt!@`#2st0ha2N($ah!IhtcH6DuR jSUH23IIB|PD2cNsCysi7^)uvWbqcApXV@Ys0-pZ?U#zkA diff --git a/java/TestFloatHolder.class b/java/TestFloatHolder.class deleted file mode 100644 index 18d7130ff77499235c562f416db1457567ae85a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 270 zcmZ9G%MQU%6h-$bRgZclMiK)v4D<)YL^KA%Kx2MeCse9U+VWWvBZ+|z@KNGiX6|60 z*S;(J^?p17%+ajEK*2)MhKUlPbyOGSL@M4pYyZeWPAE;nILxPn!eF?yP$o1z&T=|)@5g)@d3bfFtY#v diff --git a/java/TestIntHolder.class b/java/TestIntHolder.class deleted file mode 100644 index 0781a3c1f65309d2f86ed31c6e9b0f92fd7f0db3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 264 zcmZ9Gy$%6U5QWd|V*L}PM4>~$3y4MtiBQ<+@2&|eYd2ZTQ)yHZg$M9ZVn)X;&ivdn zU(W0OcmkN9R)huHL8go>a)kQ9T+ARe(QdHv4(z9d+%SlObVRT_-Hn4hq2lr>ok!^` z4!4{TGUIs5gxVsAczHg0oVdmdb#wCfd^D~}cB}_q#+*=G$LGZ7X`rUY53R2O(1s(q t^jRQHCR&iGC6%ORdw0-H8exg*xAue*%A#s*erEyL*FT>kE3>{Q#g|E=2$U diff --git a/java/TestIntegerHolder.class b/java/TestIntegerHolder.class deleted file mode 100644 index e3317aca77f71836b6fa626a271e061e6bda7bbe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 294 zcmZWj%Syvg5IvJeqcIvq+J)f4UFyPofYMD-6pAhk-QOfbOh^;BN&POnDs<5g=tqfj zMG@*O&b-b!GrP~t7Qj719~Nv6P5>7J!fB=6Ra~p4h+oo*vz{=xE1R;P5bS6?_i#Ws zOStQwnw|^Rk8Pc^COFeJXF@nD8-89cQr3w|YnhyP*&8oZqIAK(BEm2l&;CyRc-<2G z*LJBhekhG}^zY7%(clW6d~L!4nZE@OrMr|&lHKbyXeEXC37WO32}cM74fW!?7X$j( M;~C3inOQjb0*vN4`~Uy| diff --git a/java/TestListHolder.class b/java/TestListHolder.class deleted file mode 100644 index 42efc5127a208284e8f9923a338fd450a7d330c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 343 zcmZXPu};G<5QhIt+J=_W2H21oSWwGA9-vhh1Y(F_NW|`>R&bR%lH>4PFd;GY0eC3H z*<}Gd+@0<3zyI#@>thGt7RMt52qW~y=wm>LH{wMkwa``aSZriz34^85%B~3EZ2lDC zfH28rYg5(Q`=(w?L+IT!Ye|@GbzZ*UkbLhK*jP1vlg9zW3-M#w>B@*$*1f>mM$}k*6n2Onnu914n{JnE(I) diff --git a/java/TestLongHolder.class b/java/TestLongHolder.class deleted file mode 100644 index 6fa67a8691d332f4b4d63841c6fdef8f2e24297e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 267 zcmZ9G%?`m(5QWdwPt{)~b`lFaEO-I25h5ZiH1@acq$<@WRo=?NN@C#wJd~K*9d~i& z=brg;-k;|Sz#MiJ1_~yMb(Byhw2tbk#*vDH@zy=Eml4X7Fb=aRq0k@fO;iX?htJtM ziGyVl`J57pv&3hDy$)mETuv^hj&dW}w9ejvPs&kg!1+Z^SrMwcZ6SgVYn2}BG$r`@qNCOg(GyP$|yG@gyYFW zgdM^tlWTV?o-V3N-&A@bEunQ;EhJ$uS4Q5|t6bVl*7(aL?&!0f16lhJefD6 zYNvF}S_z?>s?rEoTkbmFu1`03vsvn_GG+27<2RAQBr}%jrB!VUd9D2P!+)!u`LQ@b z#E1JAfc)ufyZk$5Pwd;LZ=hEWSdXnQZ_r)zxy7SLKWIQNTU>~}hTdml&_u&;$y`s$ diff --git a/java/TestMapHolder.class b/java/TestMapHolder.class deleted file mode 100644 index 8b84b1076f34ed5dd92d05fd5bbced680ddcdcc4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 356 zcmZXPzf!_L5XQevfCz$sr85?GU|~uN7#c^%0<}oS{t|ZLG2!AI7oSTjXDoaGAIfnr z*oar`?(P1*{qyzx@d@A>;~oNp5n6q;(IJf1>P5w+GDZB5tvPpu&RiSq7KCszeT;BG z7^J*)ck21JDpza?t*dIqgyB*fzOOeK+f-$x&usH2-l$aBg1vw+o=lg`h}!8gmeh>U zO>|+DtF0(5_v`Z=-fWaOt4%TcpIJzkUQ*RI=Ns(@AOG3#!cW8*BDvMS0OYR~+m+v` ocoN?}e+RuuAic!;@(w*jUnCwq`9%YI-Qz+G8v00xVH1sh0E3oF&;S4c diff --git a/java/TestMapLongHolder.class b/java/TestMapLongHolder.class deleted file mode 100644 index cf74116bd2eb747101c3b7625d698c3123c580e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 366 zcmZXPu};H442FLvZ9_|;t&kXy7}#0{GBKs5UeNWzbI}Qjfd}BB zP<<&#sN7)t?9adLukVjf0Jj+R;K2{j>Z6SgVYn2pA}ob2!pCeWb4%z-<@+L@G=nf16lhJc*l8 zwN+)vS`nd}s6q={8}7Q^uTOV)vsq$|(na(q<2H$AtkFxV+T?Ps-00JPdtSMjxIn;H xyXS%2-E6!3J7-Vq+n4X44F{~p)|GeYA^P0n(6b-ZqqjZI$5BHcGcjnQ;TB$7PNM(- diff --git a/java/TestMapObjectHolder.class b/java/TestMapObjectHolder.class deleted file mode 100644 index c2e426968b4142212a10067e1e07a0bb64bb0e26..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 374 zcmZvXzfQw25XQet+J=@wTOlNb#K6`vkdYx(mkPvCU`WL7q*id1I>>c+E|`!QcmN)% z;+!CXDm>hs?|$F?`PzPb0=UMg2M>OLRv&G22*Z_l5n&;89zLWinOQ<-s#ULTg?wC1J2oTHaUdRGLJj1!s;o*^^w0M3`Lu$_S(JWKoZ+ ztSUn87ZJL#%C)eSVcpekecIslcCj@|=h2@`vrRPPF1@&{OeSxX8-Mc8wwG=$&JpnS x?s*`0Kie+<&e#+C_QgBsjRWq;)|DG{AAOcM^z;Yy=yiwlaZuBTObqI1_zhWlQO5uP diff --git a/java/TestNestedHolder.class b/java/TestNestedHolder.class deleted file mode 100644 index d1f9e0b71ac1466dc2648a7bab54ec8f3e03653f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 386 zcmZutu};G<5Pg@V4K0PX0}B!ZTgyN`AXOFw0x6Us5xbLE!Bv_d$KktRLSoH$>90S{pcr z&p%<01G0a?IO6JzBXJyDyo262;dP9*4Y-VlnDB^|o^oYbhh7o7q2XTiL5ZOJ4#)4zjmQiz?PUhA4rL`hInTi;#`=$hbZ%d*T2R6fqGI2-@~ diff --git a/java/TestOptionalHolder.class b/java/TestOptionalHolder.class deleted file mode 100644 index 07deabf203162d3dc308af33f0d68b942d11add0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 363 zcmZvXu};G<5QhIt+J=@w3ql|vF|f5A$OE+MQh^vMIwWFuQY*M}9ppMZ7feVDJOB@c zdQO1>@NjpwzyJQbkI%Pv0M|I`AV3(Q(M1z2LVqQmM687=;zhQSxg)eD$|yG_goELI zggwG>DmQNN=u~NhzAg1qT0&!1E+wHiS4Q4d>rC2IWSU=2Hu-~Gi&WS`{-_A2gW`wsl uOLJ`V?SeCLZjN3-FI;d#j{bXx?xV{RkDh;_0X_fXLLAieArrm2>Tdx!x=#N9 diff --git a/java/TestParamsHolder.class b/java/TestParamsHolder.class deleted file mode 100644 index 780408b676c6a1d4467334ea1fa9eac28cd8a26d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 290 zcmZ8b%WA?<5Iqy4X{}W^rF7ASyXZncpv8?;EJ#~0xajW24B<*l@A9L=OKWj=XNx4iL$*0U|GpAs7Ja2?@F zND0^e!#K067gdq5BZRXmV?u9fOMW}-Qg(?+3+eRt%45&2PK?W01n=YJ|CN<@w#=Vi zKZk2VdsQ9WhUZo{?=G*5bqfPT^0Hb0(x(NlrFxV|5}TuM(3d14C#Yl12{-5nD)m7; O)b1rs6>nt_p!)|SKRCPq diff --git a/java/TestRecordPerson.class b/java/TestRecordPerson.class deleted file mode 100644 index 0e1585005aa3ee1f7638b279c0ad1092b32c74e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1353 zcmaJ=T~pIg5IvWcrqtAaSVR;kDj#h?>i1VsXG9%4Q2r=QhCx+TkJDo6Xs?XLs-Z`Frvkz+1dZVFWP^aUG*b2xRxnL$lI0?VZYJ z*>v2NKw{OhE&ruJtW@3_LlR>eQaUa{Cwxm zFYCx4OD=PVAW>7kypAge6>3 r$`Ke?qAhR&LH~p92fbhBpDgL_a{@`+K?Qd?v1jzA=zWe=yu{=`%JLp% diff --git a/java/TestRecordWithOptional.class b/java/TestRecordWithOptional.class deleted file mode 100644 index 70cee73fe486e65acb3729026469b31ca8c2726d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1543 zcma)6T~8B16g^XDx2;=RDpavTtAe)6N5yYl;6c<#QpE_tM4z_pgf1+*b$6D;|Kx)t z5)&T$0sbiCo$fYl(}no3Gn2dLo_o)|cjnLEv)=$-Vg zw+2I7cH+?#JY&Y|DaM_V4TgpRx4di)jl+OF9XjvIB^ zv<%OS8p84TGD;GSK6EL<<}j#Rw$0r>CHEA(U|2ZUe2E4HYYcOjL#YpeV64(*Pta{j zpeP>ZIz{e;OboYZm2gRSo7PN% zI0;K%z;*|d^j6Fhq)N65?GmhLXJp3@Iu3J8(xb2t1D*!H$AHVY8-lBpfK-*%7SAvf zhh~)7Adgw{l6fSxpHN1#U?f1wt`Z~)&5c3>l{Ji`PT&MIEYe=0Q&8LC$}Z8r35vW* V_%xQ$zq25Ixl%J&Zwwjbs*fSnvVFMu>&5(3$=1R_VFYx|6H;o?A21&!>JEuS zEl$18IaPnR%N4)`l_DHuTx3hgAy24o)Is$l6^H$$zu_Py4=bNcYa)a2+8AJ zx=M|X!&wro*$}ehWX*)iT*tiFZ+$kN@*|nlcEK-im8VR|PYt2o?#y3@9!>*7ah2>% zz*B97jrSuxt3V5`d~Cx3*`Eaq(tS%N$+_+sbdo~s1nt_^gin+NEw%aIivb7x#426#g5X!$`tJHxFI*d*)1Lm_;_ z1Ri{Rk3akR|ML3>;2oVhO0aB{o2Z~l=^kP)g=9qVjHu!f^g8#O}P ze_w+g;}1-HR8(f|Me diff --git a/java/TestValidJavaHolder.class b/java/TestValidJavaHolder.class deleted file mode 100644 index d8089799f2e5cba0c6da7d02130434d648cc8d15..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 720 zcmaix%}&BV6ot>oPXSSWMKp2Y#sCXhx{$Cj8e>ov5cUI1>X6cs()wJwGI8Mp_)x|( zh!LcbF7C8vzI)E}&d2B5JAi8(Ww3*og?JVTBn9#l`6Qc;bnRw$I8mcOAlcHc4%z~- zT76(4B~a?Cc`%TU9^a9^^_;Qt1>#rUSPA4h+EowXbg2Bk96Cmsj~>-j_N8yDpcfD= z*Xo_$AWGV`FX_7qcJ%xkF8;t_pq-|XiKctnc4ZLy^gr7GTAS`|HWgR>@8xW9mcm*$ zRe~3PwFtxLh_!$1&N#lxninPQxzb@sBXT1m$s9e?^Fn{5ZnPO_HDdnfW}Y~O#rrf* z0>a!VYnoptT!m2^uMjVMp(Cr=2I8!{$g&#rn5$8D#53zEXDs)y&w&5AByy%7dGZAm XxANAKFOe_fU@M Date: Tue, 23 Jun 2026 13:38:39 -0400 Subject: [PATCH 07/13] spotless --- java/mvnw | 0 .../copilot/tool/SchemaGeneratorTest.java | 84 +++++++++++++------ 2 files changed, 58 insertions(+), 26 deletions(-) mode change 100644 => 100755 java/mvnw diff --git a/java/mvnw b/java/mvnw old mode 100644 new mode 100755 diff --git a/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java index bfbe94314..590168933 100644 --- a/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java +++ b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java @@ -8,6 +8,8 @@ import java.io.IOException; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -31,6 +33,8 @@ import javax.tools.JavaCompiler; import javax.tools.JavaFileObject; import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; import javax.tools.ToolProvider; import org.junit.jupiter.api.Test; @@ -126,6 +130,20 @@ public boolean process(Set annotations, RoundEnvironment } } + private static final Path CLASS_OUTPUT_DIR = Path.of("target", "test-schema-classes"); + + /** + * Creates a StandardJavaFileManager that writes compiled .class files to + * target/test-schema-classes/ instead of the working directory. + */ + private StandardJavaFileManager createFileManager(JavaCompiler compiler, + DiagnosticCollector diagnostics) throws IOException { + Files.createDirectories(CLASS_OUTPUT_DIR); + StandardJavaFileManager fm = compiler.getStandardFileManager(diagnostics, null, null); + fm.setLocation(StandardLocation.CLASS_OUTPUT, List.of(CLASS_OUTPUT_DIR.toFile())); + return fm; + } + private List compileAndCapture(String... sources) { return compileAndCapture(Arrays.asList(sources)); } @@ -146,26 +164,32 @@ private List compileAndCapture(List sourceTexts) { compilationUnits.add(new InMemorySource(className, sourceText)); } - // Compile with the processor on classpath - JavaCompiler.CompilationTask task = compiler.getTask(null, // writer - null, // file manager - diagnostics, // diagnostics - List.of("--add-modules", "ALL-MODULE-PATH"), // options - null, // annotation classes - compilationUnits); - - task.setProcessors(List.of(new SchemaCapturingProcessor())); - boolean success = task.call(); - - if (!success) { - // Try without module options for simpler environments - diagnostics = new DiagnosticCollector<>(); - task = compiler.getTask(null, null, diagnostics, null, null, compilationUnits); + try (StandardJavaFileManager fm = createFileManager(compiler, diagnostics)) { + // Compile with the processor on classpath + JavaCompiler.CompilationTask task = compiler.getTask(null, // writer + fm, // file manager + diagnostics, // diagnostics + List.of("--add-modules", "ALL-MODULE-PATH"), // options + null, // annotation classes + compilationUnits); + task.setProcessors(List.of(new SchemaCapturingProcessor())); - success = task.call(); - } + boolean success = task.call(); + + if (!success) { + // Try without module options for simpler environments + diagnostics = new DiagnosticCollector<>(); + try (StandardJavaFileManager fm2 = createFileManager(compiler, diagnostics)) { + task = compiler.getTask(null, fm2, diagnostics, null, null, compilationUnits); + task.setProcessors(List.of(new SchemaCapturingProcessor())); + success = task.call(); + } + } - assertTrue(success, "Compilation failed: " + diagnostics.getDiagnostics()); + assertTrue(success, "Compilation failed: " + diagnostics.getDiagnostics()); + } catch (IOException e) { + fail("Failed to create file manager: " + e.getMessage()); + } return new ArrayList<>(SchemaCapturingProcessor.capturedSchemas); } @@ -181,11 +205,15 @@ private List compileAndCaptureParams(String source) { String className = extractClassName(source); List compilationUnits = List.of(new InMemorySource(className, source)); - JavaCompiler.CompilationTask task = compiler.getTask(null, null, diagnostics, null, null, compilationUnits); - task.setProcessors(List.of(new SchemaCapturingProcessor())); - boolean success = task.call(); + try (StandardJavaFileManager fm = createFileManager(compiler, diagnostics)) { + JavaCompiler.CompilationTask task = compiler.getTask(null, fm, diagnostics, null, null, compilationUnits); + task.setProcessors(List.of(new SchemaCapturingProcessor())); + boolean success = task.call(); - assertTrue(success, "Compilation failed: " + diagnostics.getDiagnostics()); + assertTrue(success, "Compilation failed: " + diagnostics.getDiagnostics()); + } catch (IOException e) { + fail("Failed to create file manager: " + e.getMessage()); + } return new ArrayList<>(SchemaCapturingProcessor.capturedParameterSchemas); } @@ -502,11 +530,15 @@ public class TestValidJavaHolder { List compilationUnits = List .of(new InMemorySource("SchemaValidation", validationSource.toString())); - JavaCompiler.CompilationTask task = compiler.getTask(null, null, diagnostics, null, null, compilationUnits); - boolean success = task.call(); + try (StandardJavaFileManager fm = createFileManager(compiler, diagnostics)) { + JavaCompiler.CompilationTask task = compiler.getTask(null, fm, diagnostics, null, null, compilationUnits); + boolean success = task.call(); - assertTrue(success, "Generated schema source code is not valid Java: " + diagnostics.getDiagnostics() - + "\nSource:\n" + validationSource); + assertTrue(success, "Generated schema source code is not valid Java: " + diagnostics.getDiagnostics() + + "\nSource:\n" + validationSource); + } catch (IOException e) { + fail("Failed to create file manager: " + e.getMessage()); + } } @Test From 9f745e85f5b079b6012ec5e286849a0916a54f9c Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Tue, 23 Jun 2026 13:57:26 -0400 Subject: [PATCH 08/13] fix: use Map.ofEntries for properties to avoid Map.of 10-entry limit Address review comment r3461777483: Map.of() only supports up to 10 key-value pairs. Switch properties maps in SchemaGenerator to use Map.ofEntries(Map.entry(...), ...) so records/POJOs/methods with >10 fields won't cause generated source compilation failures. Update SchemaGeneratorTest expectations to match the new format. --- .../com/github/copilot/tool/SchemaGenerator.java | 12 ++++++------ .../github/copilot/tool/SchemaGeneratorTest.java | 16 +++++++++------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java b/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java index 31f78ac85..b56f27ae1 100644 --- a/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java +++ b/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java @@ -84,7 +84,7 @@ public String generateParametersSchemaSource(List par schema = generateSchema(paramType, typeUtils, elementUtils); } - propertyEntries.add("\"" + paramName + "\", " + schema); + propertyEntries.add("Map.entry(\"" + paramName + "\", " + schema + ")"); if (!isOptional) { Param paramAnnotation = param.getAnnotation(Param.class); @@ -94,7 +94,7 @@ public String generateParametersSchemaSource(List par } } - String properties = "Map.of(" + String.join(", ", propertyEntries) + ")"; + String properties = "Map.ofEntries(" + String.join(", ", propertyEntries) + ")"; String required = "List.of(" + String.join(", ", requiredNames) + ")"; return "Map.of(\"type\", \"object\", \"properties\", " + properties + ", \"required\", " + required + ")"; @@ -271,11 +271,11 @@ private String generateRecordSchema(TypeElement typeElement, Types typeUtils, El requiredNames.add("\"" + name + "\""); } - propertyEntries.add("\"" + name + "\", " + schema); + propertyEntries.add("Map.entry(\"" + name + "\", " + schema + ")"); } } - String properties = "Map.of(" + String.join(", ", propertyEntries) + ")"; + String properties = "Map.ofEntries(" + String.join(", ", propertyEntries) + ")"; String required = "List.of(" + String.join(", ", requiredNames) + ")"; return "Map.of(\"type\", \"object\", \"properties\", " + properties + ", \"required\", " + required + ")"; @@ -305,7 +305,7 @@ private String generateClassSchema(TypeElement typeElement, Types typeUtils, Ele requiredNames.add("\"" + name + "\""); } - propertyEntries.add("\"" + name + "\", " + schema); + propertyEntries.add("Map.entry(\"" + name + "\", " + schema + ")"); } } @@ -313,7 +313,7 @@ private String generateClassSchema(TypeElement typeElement, Types typeUtils, Ele return "Map.of(\"type\", \"object\")"; } - String properties = "Map.of(" + String.join(", ", propertyEntries) + ")"; + String properties = "Map.ofEntries(" + String.join(", ", propertyEntries) + ")"; String required = "List.of(" + String.join(", ", requiredNames) + ")"; return "Map.of(\"type\", \"object\", \"properties\", " + properties + ", \"required\", " + required + ")"; diff --git a/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java index 590168933..825518484 100644 --- a/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java +++ b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java @@ -453,8 +453,9 @@ public record TestRecordPerson(String name, int age, boolean active) {} """; List schemas = compileAndCapture(source); String expected = "Map.of(\"type\", \"object\", \"properties\", " - + "Map.of(\"name\", Map.of(\"type\", \"string\"), " + "\"age\", Map.of(\"type\", \"integer\"), " - + "\"active\", Map.of(\"type\", \"boolean\")), " + + "Map.ofEntries(Map.entry(\"name\", Map.of(\"type\", \"string\")), " + + "Map.entry(\"age\", Map.of(\"type\", \"integer\")), " + + "Map.entry(\"active\", Map.of(\"type\", \"boolean\"))), " + "\"required\", List.of(\"name\", \"age\", \"active\"))"; assertContainsSchema(schemas, "TestRecordPerson", expected); } @@ -467,8 +468,8 @@ public record TestRecordWithOptional(String name, Optional nickname) {} """; List schemas = compileAndCapture(source); String expected = "Map.of(\"type\", \"object\", \"properties\", " - + "Map.of(\"name\", Map.of(\"type\", \"string\"), " + "\"nickname\", Map.of(\"type\", \"string\")), " - + "\"required\", List.of(\"name\"))"; + + "Map.ofEntries(Map.entry(\"name\", Map.of(\"type\", \"string\")), " + + "Map.entry(\"nickname\", Map.of(\"type\", \"string\"))), " + "\"required\", List.of(\"name\"))"; assertContainsSchema(schemas, "TestRecordWithOptional", expected); } @@ -483,10 +484,11 @@ public void parametersTarget(String query, int limit, boolean verbose) {} assertFalse(paramSchemas.isEmpty(), "Expected parameter schemas"); String schema = paramSchemas.get(0); assertTrue(schema.contains("\"type\", \"object\""), "Should be object type: " + schema); - assertTrue(schema.contains("\"query\", Map.of(\"type\", \"string\")"), "Should have query property: " + schema); - assertTrue(schema.contains("\"limit\", Map.of(\"type\", \"integer\")"), + assertTrue(schema.contains("Map.entry(\"query\", Map.of(\"type\", \"string\"))"), + "Should have query property: " + schema); + assertTrue(schema.contains("Map.entry(\"limit\", Map.of(\"type\", \"integer\"))"), "Should have limit property: " + schema); - assertTrue(schema.contains("\"verbose\", Map.of(\"type\", \"boolean\")"), + assertTrue(schema.contains("Map.entry(\"verbose\", Map.of(\"type\", \"boolean\"))"), "Should have verbose property: " + schema); assertTrue(schema.contains("\"required\", List.of("), "Should have required list: " + schema); } From 52a5bbc8e6d25924f7f3fa0c635fedeb8c485c61 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Tue, 23 Jun 2026 14:04:49 -0400 Subject: [PATCH 09/13] fix: add missing Byte/Short/Character boxed type mappings Address review comment r3461777428: Byte and Short now map to "integer", Character maps to "string", matching their primitive equivalents. Add tests for all three. --- .../github/copilot/tool/SchemaGenerator.java | 6 +++- .../copilot/tool/SchemaGeneratorTest.java | 33 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java b/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java index b56f27ae1..8d7c8c586 100644 --- a/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java +++ b/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java @@ -152,7 +152,8 @@ private String generateDeclaredTypeSchema(DeclaredType type, Types typeUtils, El } // Boxed primitives - if ("java.lang.Integer".equals(qualifiedName) || "java.lang.Long".equals(qualifiedName)) { + if ("java.lang.Integer".equals(qualifiedName) || "java.lang.Long".equals(qualifiedName) + || "java.lang.Byte".equals(qualifiedName) || "java.lang.Short".equals(qualifiedName)) { return "Map.of(\"type\", \"integer\")"; } if ("java.lang.Double".equals(qualifiedName) || "java.lang.Float".equals(qualifiedName)) { @@ -161,6 +162,9 @@ private String generateDeclaredTypeSchema(DeclaredType type, Types typeUtils, El if ("java.lang.Boolean".equals(qualifiedName)) { return "Map.of(\"type\", \"boolean\")"; } + if ("java.lang.Character".equals(qualifiedName)) { + return "Map.of(\"type\", \"string\")"; + } // UUID if ("java.util.UUID".equals(qualifiedName)) { diff --git a/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java index 825518484..deb981ef8 100644 --- a/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java +++ b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java @@ -323,6 +323,39 @@ public class TestBooleanBoxedHolder { assertContainsSchema(schemas, "schemaTargetBooleanBoxed", "Map.of(\"type\", \"boolean\")"); } + @Test + void byteBoxedType() { + String source = """ + public class TestByteHolder { + public Byte schemaTargetByte() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema(schemas, "schemaTargetByte", "Map.of(\"type\", \"integer\")"); + } + + @Test + void shortBoxedType() { + String source = """ + public class TestShortHolder { + public Short schemaTargetShort() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema(schemas, "schemaTargetShort", "Map.of(\"type\", \"integer\")"); + } + + @Test + void characterBoxedType() { + String source = """ + public class TestCharHolder { + public Character schemaTargetChar() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema(schemas, "schemaTargetChar", "Map.of(\"type\", \"string\")"); + } + @Test void stringArrayType() { String source = """ From 79063d7e6433e373b3ea6a23c72b1fb01a8e7ba9 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Tue, 23 Jun 2026 14:10:28 -0400 Subject: [PATCH 10/13] fix: add missing OptionalLong mapping in generateDeclaredTypeSchema Address review comment r3461777459: OptionalLong was handled in isOptionalType/unwrapOptional but missing from generateDeclaredTypeSchema, causing it to fall through to POJO introspection when used as a direct return type. Add the mapping and tests for OptionalInt, OptionalLong, and OptionalDouble. --- .../github/copilot/tool/SchemaGenerator.java | 3 ++ .../copilot/tool/SchemaGeneratorTest.java | 36 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java b/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java index 8d7c8c586..8f9261018 100644 --- a/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java +++ b/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java @@ -200,6 +200,9 @@ private String generateDeclaredTypeSchema(DeclaredType type, Types typeUtils, El if ("java.util.OptionalDouble".equals(qualifiedName)) { return "Map.of(\"type\", \"number\")"; } + if ("java.util.OptionalLong".equals(qualifiedName)) { + return "Map.of(\"type\", \"integer\")"; + } // List / Collection if (isCollectionType(qualifiedName)) { diff --git a/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java index deb981ef8..19cac1163 100644 --- a/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java +++ b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java @@ -454,6 +454,42 @@ public class TestOptionalHolder { assertContainsSchema(schemas, "schemaTargetOptional", "Map.of(\"type\", \"string\")"); } + @Test + void optionalIntType() { + String source = """ + import java.util.OptionalInt; + public class TestOptionalIntHolder { + public OptionalInt schemaTargetOptionalInt() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema(schemas, "schemaTargetOptionalInt", "Map.of(\"type\", \"integer\")"); + } + + @Test + void optionalLongType() { + String source = """ + import java.util.OptionalLong; + public class TestOptionalLongHolder { + public OptionalLong schemaTargetOptionalLong() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema(schemas, "schemaTargetOptionalLong", "Map.of(\"type\", \"integer\")"); + } + + @Test + void optionalDoubleType() { + String source = """ + import java.util.OptionalDouble; + public class TestOptionalDoubleHolder { + public OptionalDouble schemaTargetOptionalDouble() { return null; } + } + """; + List schemas = compileAndCapture(source); + assertContainsSchema(schemas, "schemaTargetOptionalDouble", "Map.of(\"type\", \"number\")"); + } + @Test void uuidType() { String source = """ From 469b5f438a20c8d01925804e2746c62aaacb4a31 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Tue, 23 Jun 2026 14:12:46 -0400 Subject: [PATCH 11/13] fix: correct misleading @JsonSubTypes comment on sealed interface handling Address review comment r3461777579: the implementation uses getPermittedSubclasses() (Java sealed types), not Jackson annotations. --- java/src/main/java/com/github/copilot/tool/SchemaGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java b/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java index 8f9261018..f2c92df85 100644 --- a/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java +++ b/java/src/main/java/com/github/copilot/tool/SchemaGenerator.java @@ -250,7 +250,7 @@ private String generateDeclaredTypeSchema(DeclaredType type, Types typeUtils, El return generateClassSchema(typeElement, typeUtils, elementUtils); } - // Sealed interfaces with @JsonSubTypes — oneOf + // Sealed interfaces — oneOf via permitted subclasses if (typeElement.getKind() == ElementKind.INTERFACE) { return generateSealedSchema(typeElement, typeUtils, elementUtils); } From aca417728ad445a03d0f997b82ba27b923f723b1 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Tue, 23 Jun 2026 14:23:16 -0400 Subject: [PATCH 12/13] test: add sealed interface test for oneOf schema generation Address review comment r3461777685: the processor had special handling for TestSealed* types but no test exercised generateSealedSchema(). Add a test with a sealed interface (TestSealedShape) and two record permits (Circle, Rect) verifying the oneOf schema output. --- .../copilot/tool/SchemaGeneratorTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java index 19cac1163..89e1da940 100644 --- a/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java +++ b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java @@ -638,6 +638,27 @@ public class TestObjectHolder { assertContainsSchema(schemas, "schemaTargetObject", "Map.of()"); } + @Test + void sealedInterfaceType() { + String sealedInterface = """ + public sealed interface TestSealedShape permits TestSealedCircle, TestSealedRect {} + """; + String circle = """ + public record TestSealedCircle(double radius) implements TestSealedShape {} + """; + String rect = """ + public record TestSealedRect(double width, double height) implements TestSealedShape {} + """; + List schemas = compileAndCapture(sealedInterface, circle, rect); + String expected = "Map.of(\"oneOf\", List.of(" + "Map.of(\"type\", \"object\", \"properties\", " + + "Map.ofEntries(Map.entry(\"radius\", Map.of(\"type\", \"number\"))), " + + "\"required\", List.of(\"radius\")), " + "Map.of(\"type\", \"object\", \"properties\", " + + "Map.ofEntries(Map.entry(\"width\", Map.of(\"type\", \"number\")), " + + "Map.entry(\"height\", Map.of(\"type\", \"number\"))), " + + "\"required\", List.of(\"width\", \"height\"))))"; + assertContainsSchema(schemas, "TestSealedShape", expected); + } + private void assertContainsSchema(List schemas, String methodName, String expectedSchema) { String expected = methodName + "=" + expectedSchema; assertTrue(schemas.stream().anyMatch(s -> s.equals(expected)), From d5feff27b3ecacea49e1951420c77621452ef7c6 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Tue, 23 Jun 2026 14:25:50 -0400 Subject: [PATCH 13/13] test: add >10-field record test proving Map.ofEntries compiles Address review comment r3461777706: add a test with an 11-component record that verifies the generated Map.ofEntries(...) expression actually compiles, proving the Map.of 10-entry limit fix works end-to-end. --- .../copilot/tool/SchemaGeneratorTest.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java index 89e1da940..8e024ab9f 100644 --- a/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java +++ b/java/src/test/java/com/github/copilot/tool/SchemaGeneratorTest.java @@ -542,6 +542,39 @@ public record TestRecordWithOptional(String name, Optional nickname) {} assertContainsSchema(schemas, "TestRecordWithOptional", expected); } + @Test + void recordWithMoreThanTenFields() { + String source = """ + public record TestRecordLarge( + String f1, String f2, String f3, String f4, String f5, + String f6, String f7, String f8, String f9, String f10, + String f11) {} + """; + List schemas = compileAndCapture(source); + // Verify the schema contains all 11 fields and uses Map.ofEntries + String schema = schemas.stream().filter(s -> s.startsWith("TestRecordLarge=")).findFirst().orElse(""); + assertFalse(schema.isEmpty(), "Expected schema for TestRecordLarge"); + assertTrue(schema.contains("Map.ofEntries("), "Should use Map.ofEntries for >10 fields: " + schema); + assertTrue(schema.contains("Map.entry(\"f1\""), "Should have f1: " + schema); + assertTrue(schema.contains("Map.entry(\"f11\""), "Should have f11: " + schema); + // Verify the generated source expression is compilable by re-compiling it + String schemaExpr = schema.substring(schema.indexOf('=') + 1); + String validationSource = "import java.util.Map;\nimport java.util.List;\n" + + "public class LargeRecordValidation {\n" + " @SuppressWarnings(\"unchecked\")\n" + + " public Object schema() { return " + schemaExpr + "; }\n}\n"; + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + List units = List.of(new InMemorySource("LargeRecordValidation", validationSource)); + try (StandardJavaFileManager fm = createFileManager(compiler, diagnostics)) { + JavaCompiler.CompilationTask task = compiler.getTask(null, fm, diagnostics, null, null, units); + boolean success = task.call(); + assertTrue(success, "Generated schema for >10-field record does not compile: " + + diagnostics.getDiagnostics() + "\nSource:\n" + validationSource); + } catch (IOException e) { + fail("Failed to create file manager: " + e.getMessage()); + } + } + @Test void parametersSchema() { String source = """