Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ func NewDecoder(r io.Reader) *jsontext.Decoder {

type (
Value = jsontext.Value
Kind = jsontext.Kind
UnmarshalerFrom = json.UnmarshalerFrom
MarshalerTo = json.MarshalerTo
Decoder = jsontext.Decoder
Expand Down
108 changes: 81 additions & 27 deletions internal/lsp/lsproto/_generate/generate.mts
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,40 @@ function goKindCasesForJsonKind(kind: string): string {
}
}

/**
* Checks if a meta model Type can represent a JSON null value.
* Used to determine whether to reject explicit JSON `null` for any field
* that can otherwise decode `null` without a type error.
*/
function typeCanBeNull(type: Type): boolean {
switch (type.kind) {
case "base":
return type.name === "null";
case "reference": {
const override = typeAliasOverrides.get(type.name);
if (override) {
return override.name === "any";
}
// A bare "any" reference resolves to Go's `any` (interface), which can hold null.
if (type.name === "any") {
return true;
}
if (nonResolvedAliases.has(type.name)) {
const customAlias = customTypeAliases.find(t => t.name === type.name);
if (customAlias) return typeCanBeNull(customAlias.type);
return false;
}
const aliased = typeInfo.typeAliasMap.get(type.name);
if (aliased) return typeCanBeNull(aliased);
return false;
}
case "or":
return type.items.some(item => typeCanBeNull(item));
default:
return false;
}
}

/**
* For a group of union entries that share the same JSON kind (e.g., all objects),
* find a discriminator field — a JSON property whose string literal type differs
Expand Down Expand Up @@ -1728,24 +1762,33 @@ function generateCode() {
if (p.omitzeroValue) return false;
return true;
}) || [];
if (requiredProps.length > 0 && structure.name !== "Registration") {
// Check if any fields need null rejection
const hasNullRejectableFields = structure.properties?.some(p => {
if (p.omitzeroValue) return false;
if (typeCanBeNull(p.type)) return false;
const resolved = resolveType(p.type);
return p.optional || resolved.needsPointer || resolved.name.startsWith("[]") || resolved.name.startsWith("map[");
}) || false;
if ((requiredProps.length > 0 || hasNullRejectableFields) && structure.name !== "Registration") {
writeLine(`\tvar _ json.UnmarshalerFrom = (*${structure.name})(nil)`);
writeLine("");

writeLine(`func (s *${structure.name}) UnmarshalJSONFrom(dec *json.Decoder) error {`);
writeLine(`\tconst (`);
for (let i = 0; i < requiredProps.length; i++) {
const prop = requiredProps[i];
const iotaPrefix = i === 0 ? " uint = 1 << iota" : "";
writeLine(`\t\tmissing${titleCase(prop.name)}${iotaPrefix}`);
if (requiredProps.length > 0) {
writeLine(`\tconst (`);
for (let i = 0; i < requiredProps.length; i++) {
const prop = requiredProps[i];
const iotaPrefix = i === 0 ? " uint = 1 << iota" : "";
writeLine(`\t\tmissing${titleCase(prop.name)}${iotaPrefix}`);
}
writeLine(`\t\t_missingLast`);
writeLine(`\t)`);
writeLine(`\tmissing := _missingLast - 1`);
writeLine("");
}
writeLine(`\t\t_missingLast`);
writeLine(`\t)`);
writeLine(`\tmissing := _missingLast - 1`);
writeLine("");

writeLine(`\tif k := dec.PeekKind(); k != '{' {`);
writeLine(`\t\treturn fmt.Errorf("expected object start, but encountered %v", k)`);
writeLine(`\t\treturn errNotObject(k)`);
writeLine(`\t}`);
writeLine(`\tif _, err := dec.ReadToken(); err != nil {`);
writeLine(`\t\treturn err`);
Expand All @@ -1764,6 +1807,15 @@ function generateCode() {
if (!prop.optional && !prop.omitzeroValue) {
writeLine(`\t\t\tmissing &^= missing${titleCase(prop.name)}`);
}
// Reject null for fields whose types cannot represent null but whose Go types
// silently accept it (pointers, slices, maps).
const resolvedType = resolveType(prop.type);
const goTypeAcceptsNull = (prop.optional || resolvedType.needsPointer || resolvedType.name.startsWith("[]") || resolvedType.name.startsWith("map[")) && !prop.omitzeroValue;
if (goTypeAcceptsNull && !typeCanBeNull(prop.type)) {
writeLine(`\t\t\tif dec.PeekKind() == 'n' {`);
writeLine(`\t\t\t\treturn errNull("${prop.name}")`);
writeLine(`\t\t\t}`);
}
writeLine(`\t\t\tif err := json.UnmarshalDecode(dec, &s.${titleCase(prop.name)}); err != nil {`);
writeLine(`\t\t\t\treturn err`);
writeLine(`\t\t\t}`);
Expand All @@ -1782,17 +1834,19 @@ function generateCode() {
writeLine(`\t}`);
writeLine("");

writeLine(`\tif missing != 0 {`);
writeLine(`\t\tvar missingProps []string`);
for (const prop of requiredProps) {
writeLine(`\t\tif missing&missing${titleCase(prop.name)} != 0 {`);
writeLine(`\t\t\tmissingProps = append(missingProps, "${prop.name}")`);
writeLine(`\t\t}`);
if (requiredProps.length > 0) {
writeLine(`\tif missing != 0 {`);
writeLine(`\t\tvar missingProps []string`);
for (const prop of requiredProps) {
writeLine(`\t\tif missing&missing${titleCase(prop.name)} != 0 {`);
writeLine(`\t\t\tmissingProps = append(missingProps, "${prop.name}")`);
writeLine(`\t\t}`);
}
writeLine(`\t\treturn errMissing(missingProps)`);
writeLine(`\t}`);
writeLine("");
}
writeLine(`\t\treturn fmt.Errorf("missing required properties: %s", strings.Join(missingProps, ", "))`);
writeLine(`\t}`);

writeLine("");
writeLine(`\treturn nil`);
writeLine(`}`);
writeLine("");
Expand Down Expand Up @@ -1872,7 +1926,7 @@ function generateCode() {
writeLine(`\tmissing := _missingLast - 1`);
writeLine("");
writeLine(`\tif k := dec.PeekKind(); k != '{' {`);
writeLine(`\t\treturn fmt.Errorf("expected object start, but encountered %v", k)`);
writeLine(`\t\treturn errNotObject(k)`);
writeLine(`\t}`);
writeLine(`\tif _, err := dec.ReadToken(); err != nil {`);
writeLine(`\t\treturn err`);
Expand Down Expand Up @@ -1922,7 +1976,7 @@ function generateCode() {
writeLine(`\t\tif missing&missingMethod != 0 {`);
writeLine(`\t\t\tmissingProps = append(missingProps, "method")`);
writeLine(`\t\t}`);
writeLine(`\t\treturn fmt.Errorf("missing required properties: %s", strings.Join(missingProps, ", "))`);
writeLine(`\t\treturn errMissing(missingProps)`);
writeLine(`\t}`);
writeLine("");
writeLine(`\tif len(rawRegisterOptions) > 0 {`);
Expand Down Expand Up @@ -2619,7 +2673,7 @@ function generateCode() {
}

writeLine(`\tdefault:`);
writeLine(`\t\treturn fmt.Errorf("invalid ${name}: expected ${[...(unionContainedNull ? ["null"] : []), ...kindMap.keys()].join(", ")}, got %v", dec.PeekKind())`);
writeLine(`\t\treturn errInvalidKind("${name}", dec.PeekKind())`);
writeLine(`\t}`);
}
else if (canDispatch) {
Expand Down Expand Up @@ -2681,13 +2735,13 @@ function generateCode() {
}
}
if (!exhaustive) {
writeLine(`\t\treturn fmt.Errorf("invalid ${name}: %s", data)`);
writeLine(`\t\treturn errInvalidValue("${name}", data)`);
}
}
}

writeLine(`\tdefault:`);
writeLine(`\t\treturn fmt.Errorf("invalid ${name}: expected ${[...(unionContainedNull ? ["null"] : []), ...kindMap.keys()].join(", ")}, got %v", dec.PeekKind())`);
writeLine(`\t\treturn errInvalidKind("${name}", dec.PeekKind())`);
writeLine(`\t}`);
}
else {
Expand Down Expand Up @@ -2732,7 +2786,7 @@ function generateCode() {
}
else if (!fallbackExhaustive) {
// Fallback paths: the final error references `data` which is in scope.
writeLine(`\treturn fmt.Errorf("invalid ${name}: %s", data)`);
writeLine(`\treturn errInvalidValue("${name}", data)`);
}
writeLine(`}`);
writeLine("");
Expand Down Expand Up @@ -2773,7 +2827,7 @@ function generateCode() {
writeLine(`\t\treturn err`);
writeLine(`\t}`);
writeLine(`\tif string(v) != \`${jsonValue}\` {`);
writeLine(`\t\treturn fmt.Errorf("expected ${name} value %s, got %s", \`${jsonValue}\`, v)`);
writeLine(`\t\treturn errLiteralMismatch("${name}", \`${jsonValue}\`, v)`);
writeLine(`\t}`);
writeLine(`\treturn nil`);
writeLine(`}`);
Expand Down
24 changes: 24 additions & 0 deletions internal/lsp/lsproto/lsp.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,30 @@ func boolToInt(b bool) int {
return 0
}

func errNotObject(k json.Kind) error {
return fmt.Errorf("expected object start, but encountered %v", k)
}

func errNull(field string) error {
return fmt.Errorf("null value is not allowed for field %q", field)
}

func errMissing(props []string) error {
return fmt.Errorf("missing required properties: %s", strings.Join(props, ", "))
}

func errInvalidKind(typeName string, got json.Kind) error {
return fmt.Errorf("invalid %s: got %v", typeName, got)
}

func errInvalidValue(typeName string, data []byte) error {
return fmt.Errorf("invalid %s: %s", typeName, data)
}

func errLiteralMismatch(typeName string, expected string, got []byte) error {
return fmt.Errorf("expected %s value %s, got %s", typeName, expected, got)
}

func assertOnlyOne(message string, count int) {
if count != 1 {
panic(message)
Expand Down
Loading
Loading