Overview
Add compile-time validation in CopilotToolProcessor that checks whether a @Param(defaultValue="...") string is parseable for the annotated parameter's type. Currently, an invalid default (e.g., @Param(defaultValue="abc") on an int parameter) will surface as either a compiler error in the generated $$CopilotToolMeta code or a runtime coercion error — and the error message will point at the generated class instead of the original annotation site.
This enhancement reports the error directly on the @Param annotation via Messager.printMessage(ERROR, ...), giving developers a clear message at the source of the problem.
Branch: edburns/1682-java-tool-ergonomics on upstream (⚠️ NOT main — PRs must target this branch)
Origin: PR #1787 review comment r3469523818
Prerequisites
• Tasks 4.1–4.4 must be complete and merged to the branch (they are).
• Before writing any code, read the existing validation logic in CopilotToolProcessor.java (lines 65–73 for the required=true + defaultValue conflict check).
Deliverables
File to modify
java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java — add a validateDefaultValue(VariableElement param, TypeMirror type, String defaultValue) method and call it from the existing @Param validation loop.
Implementation specification
Add validation in the existing @Param validation loop (currently at lines 65–73) that checks parseability when defaultValue is non-empty. The validation must cover:
| Parameter type |
Validation |
int / java.lang.Integer |
Integer.parseInt(defaultValue) succeeds |
long / java.lang.Long |
Long.parseLong(defaultValue) succeeds |
short / java.lang.Short |
Short.parseShort(defaultValue) succeeds |
byte / java.lang.Byte |
Byte.parseByte(defaultValue) succeeds |
float / java.lang.Float |
Float.parseFloat(defaultValue) succeeds |
double / java.lang.Double |
Double.parseDouble(defaultValue) succeeds |
boolean / java.lang.Boolean |
Value is exactly "true" or "false" (case-insensitive) |
char / java.lang.Character |
defaultValue.length() == 1 |
java.lang.String |
Always valid — skip validation |
| Enum types |
Value matches one of the enum constants (use TypeElement.getEnclosedElements() filtered by ElementKind.ENUM_CONSTANT) |
For types not in this table (records, POJOs, java.time.*, collections, etc.), do NOT validate — emit no error. These types use Jackson convertValue() at runtime and their string representation is too complex for compile-time validation.
Error message format
@Param defaultValue "<value>" is not a valid <type>. Expected: <hint>.
Examples:
@Param defaultValue "abc" is not a valid int. Expected: an integer literal (e.g., "42").
@Param defaultValue "maybe" is not a valid boolean. Expected: "true" or "false".
@Param defaultValue "UNKNOWN" is not a valid com.example.Color. Expected one of: RED, GREEN, BLUE.
Where to integrate
In the existing validation loop in process():
// Validate @Param conflicts
for (VariableElement param : method.getParameters()) {
Param paramAnnotation = param.getAnnotation(Param.class);
if (paramAnnotation != null && paramAnnotation.required()
&& !paramAnnotation.defaultValue().isEmpty()) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@Param cannot have both required=true and a non-empty defaultValue", param);
}
// ADD HERE: validate defaultValue parseability
if (paramAnnotation != null && !paramAnnotation.defaultValue().isEmpty()) {
validateDefaultValue(param, param.asType(), paramAnnotation.defaultValue());
}
}
Helper method skeleton
/**
* Validates that a @Param defaultValue string can be parsed as the given parameter type.
* Emits a compile error on the parameter if validation fails.
* Types that cannot be validated at compile time (records, POJOs, java.time.*, etc.)
* are silently skipped.
*/
private void validateDefaultValue(VariableElement param, TypeMirror type, String defaultValue) {
// Handle primitives
if (type.getKind().isPrimitive()) {
switch (type.getKind()) {
case INT:
tryParse(() -> Integer.parseInt(defaultValue), param, "int", "an integer literal (e.g., \"42\")");
break;
case LONG:
tryParse(() -> Long.parseLong(defaultValue), param, "long", "a long literal (e.g., \"100\")");
break;
// ... short, byte, float, double, boolean, char
}
return;
}
// Handle declared types (boxed, String, enums)
if (type.getKind() == TypeKind.DECLARED) {
TypeElement typeElement = (TypeElement) ((DeclaredType) type).asElement();
String qualifiedName = typeElement.getQualifiedName().toString();
if ("java.lang.String".equals(qualifiedName)) {
return; // always valid
}
if ("java.lang.Integer".equals(qualifiedName)) {
tryParse(() -> Integer.parseInt(defaultValue), param, "Integer", "an integer literal");
return;
}
// ... other boxed types, Boolean, Character
if (typeElement.getKind() == ElementKind.ENUM) {
validateEnumDefault(param, typeElement, defaultValue);
return;
}
}
// Other types: skip validation (too complex for compile time)
}
private void tryParse(Runnable parser, VariableElement param, String typeName, String hint) {
try {
parser.run();
} catch (NumberFormatException e) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@Param defaultValue \"" + /* defaultValue */ + "\" is not a valid " + typeName
+ ". Expected: " + hint + ".", param);
}
}
private void validateEnumDefault(VariableElement param, TypeElement enumType, String defaultValue) {
List<String> constants = enumType.getEnclosedElements().stream()
.filter(e -> e.getKind() == ElementKind.ENUM_CONSTANT)
.map(e -> e.getSimpleName().toString())
.collect(Collectors.toList());
if (!constants.contains(defaultValue)) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@Param defaultValue \"" + defaultValue + "\" is not a valid "
+ enumType.getQualifiedName() + ". Expected one of: "
+ String.join(", ", constants) + ".", param);
}
}
Gating tests and criteria
All of the following must pass before this task is considered complete:
- Invalid int default test: Compile a class with
@Param(defaultValue="abc") int count — verify compile ERROR with message containing "not a valid int".
- Invalid long default test:
@Param(defaultValue="not_a_long") long id — verify compile ERROR.
- Invalid boolean default test:
@Param(defaultValue="maybe") boolean flag — verify compile ERROR with message mentioning "true" or "false".
- Invalid char default test:
@Param(defaultValue="ab") char c — verify compile ERROR (length != 1).
- Invalid enum default test:
@Param(defaultValue="UNKNOWN") MyEnum e where MyEnum has {RED, GREEN, BLUE} — verify compile ERROR listing the valid constants.
- Valid int default test:
@Param(defaultValue="42") int count — verify NO compile error (compilation succeeds).
- Valid boolean default test:
@Param(defaultValue="true") boolean flag — verify NO compile error.
- Valid enum default test:
@Param(defaultValue="RED") MyEnum e — verify NO compile error.
- Skipped complex type test:
@Param(defaultValue="2024-01-01T00:00:00Z") java.time.OffsetDateTime dt — verify NO compile error (validation is skipped for non-primitive, non-boxed, non-enum types).
- Valid String default test:
@Param(defaultValue="anything") String s — verify NO compile error.
- Boxed types test:
@Param(defaultValue="xyz") Integer n — verify compile ERROR.
- Existing required+defaultValue conflict still works: Ensure the existing
required=true + defaultValue validation is unchanged and still reports its own error.
- Spotless format check:
mvn spotless:check passes.
- Full test suite:
mvn clean verify passes (existing tests not broken).
Constraints
• ✅✅ YOU MUST run mvn spotless:apply before every commit.
• Do NOT modify any files outside the java/ directory.
• Follow existing code style (4-space indent, Javadoc on public APIs).
• Add tests to the existing CopilotToolProcessorTest.java test class.
• The isBoxedNumeric() helper already exists in CopilotToolProcessor — reuse it for the boxed numeric type checks.
• The validation must NOT reject complex types it cannot validate — silently skip them.
• Boolean validation must be case-insensitive ("True", "FALSE", "true" are all valid).
Overview
Add compile-time validation in
CopilotToolProcessorthat checks whether a@Param(defaultValue="...")string is parseable for the annotated parameter's type. Currently, an invalid default (e.g.,@Param(defaultValue="abc")on anintparameter) will surface as either a compiler error in the generated$$CopilotToolMetacode or a runtime coercion error — and the error message will point at the generated class instead of the original annotation site.This enhancement reports the error directly on the
@Paramannotation viaMessager.printMessage(ERROR, ...), giving developers a clear message at the source of the problem.Branch:⚠️ NOT
edburns/1682-java-tool-ergonomicsonupstream(main— PRs must target this branch)Origin: PR #1787 review comment r3469523818
Prerequisites
• Tasks 4.1–4.4 must be complete and merged to the branch (they are).
• Before writing any code, read the existing validation logic in
CopilotToolProcessor.java(lines 65–73 for therequired=true+defaultValueconflict check).Deliverables
File to modify
java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java— add avalidateDefaultValue(VariableElement param, TypeMirror type, String defaultValue)method and call it from the existing@Paramvalidation loop.Implementation specification
Add validation in the existing
@Paramvalidation loop (currently at lines 65–73) that checks parseability whendefaultValueis non-empty. The validation must cover:int/java.lang.IntegerInteger.parseInt(defaultValue)succeedslong/java.lang.LongLong.parseLong(defaultValue)succeedsshort/java.lang.ShortShort.parseShort(defaultValue)succeedsbyte/java.lang.ByteByte.parseByte(defaultValue)succeedsfloat/java.lang.FloatFloat.parseFloat(defaultValue)succeedsdouble/java.lang.DoubleDouble.parseDouble(defaultValue)succeedsboolean/java.lang.Boolean"true"or"false"(case-insensitive)char/java.lang.CharacterdefaultValue.length() == 1java.lang.StringTypeElement.getEnclosedElements()filtered byElementKind.ENUM_CONSTANT)For types not in this table (records, POJOs,
java.time.*, collections, etc.), do NOT validate — emit no error. These types use JacksonconvertValue()at runtime and their string representation is too complex for compile-time validation.Error message format
Examples:
@Param defaultValue "abc" is not a valid int. Expected: an integer literal (e.g., "42").@Param defaultValue "maybe" is not a valid boolean. Expected: "true" or "false".@Param defaultValue "UNKNOWN" is not a valid com.example.Color. Expected one of: RED, GREEN, BLUE.Where to integrate
In the existing validation loop in
process():Helper method skeleton
Gating tests and criteria
All of the following must pass before this task is considered complete:
@Param(defaultValue="abc") int count— verify compile ERROR with message containing"not a valid int".@Param(defaultValue="not_a_long") long id— verify compile ERROR.@Param(defaultValue="maybe") boolean flag— verify compile ERROR with message mentioning"true" or "false".@Param(defaultValue="ab") char c— verify compile ERROR (length != 1).@Param(defaultValue="UNKNOWN") MyEnum ewhereMyEnumhas{RED, GREEN, BLUE}— verify compile ERROR listing the valid constants.@Param(defaultValue="42") int count— verify NO compile error (compilation succeeds).@Param(defaultValue="true") boolean flag— verify NO compile error.@Param(defaultValue="RED") MyEnum e— verify NO compile error.@Param(defaultValue="2024-01-01T00:00:00Z") java.time.OffsetDateTime dt— verify NO compile error (validation is skipped for non-primitive, non-boxed, non-enum types).@Param(defaultValue="anything") String s— verify NO compile error.@Param(defaultValue="xyz") Integer n— verify compile ERROR.required=true+defaultValuevalidation is unchanged and still reports its own error.mvn spotless:checkpasses.mvn clean verifypasses (existing tests not broken).Constraints
• ✅✅ YOU MUST run
mvn spotless:applybefore every commit.• Do NOT modify any files outside the
java/directory.• Follow existing code style (4-space indent, Javadoc on public APIs).
• Add tests to the existing
CopilotToolProcessorTest.javatest class.• The
isBoxedNumeric()helper already exists inCopilotToolProcessor— reuse it for the boxed numeric type checks.• The validation must NOT reject complex types it cannot validate — silently skip them.
• Boolean validation must be case-insensitive (
"True","FALSE","true"are all valid).