[Java] @CopilotTool ergonomics: Validate @Param defaultValue parseability at compile time
@edburns ya está trabajando en esto.
Desde el 24/6/2026.
Evaluación
Este issue todavía no se ha evaluado.
Descripción
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 avalidateDefaultValue(VariableElement param, TypeMirror type, String defaultValue)method and call it from the existing@Paramvalidation 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 ewhereMyEnumhas{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+defaultValuevalidation is unchanged and still reports its own error. - Spotless format check:
mvn spotless:checkpasses. - Full test suite:
mvn clean verifypasses (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).
- Lenguaje dominante
- Java
- Estrellas
- 10.5k
- Forks
- 1.5k
- Merge medio
- 1 d 9 h
- PR fusionados (30 d)
- 131
Guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de github/copilot-sdk
-
agentic-workflows
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
github/copilot-sdk#2709 · 1 comentario ·
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 78/100
github/copilot-sdk#2673 ·
-
bug testing
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
github/copilot-sdk#2628 ·
-
agentic-workflows
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
github/copilot-sdk#2627 · 1 comentario ·
-
agentic-workflows
Dificultad 2/5 1-3 horas Aptitud para principiantes 74/100
github/copilot-sdk#2493 ·
Todos los issues de github/copilot-sdk
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
elastic/gradle-plugins#157 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
cryptomator/hub#497 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
johanhaleby/occurrent#1120 ·