diff --git a/generator-plugins/dotnet/custom/CustomObjectConverter.cs b/generator-plugins/dotnet/custom/CustomObjectConverter.cs new file mode 100644 index 0000000..4245c1a --- /dev/null +++ b/generator-plugins/dotnet/custom/CustomObjectConverter.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; + + +class CustomObjectConverter : JsonConverter where T : Dictionary +{ + public override T ReadJson(JsonReader reader, Type objectType, T? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return default(T); + } + + Dictionary? o = serializer.Deserialize>(reader); + if (o == null) + { + return default(T); + } + return (T)Activator.CreateInstance(typeof(T), o) ?? default(T); + } + + public override void WriteJson(JsonWriter writer, T? value, JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + } + else + { + writer.WriteStartObject(); + foreach (var kvp in value) + { + writer.WritePropertyName(kvp.Key); + serializer.Serialize(writer, kvp.Value); + } + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/generator-plugins/dotnet/custom/CustomStringConverter.cs b/generator-plugins/dotnet/custom/CustomStringConverter.cs index b6e1914..33a04df 100644 --- a/generator-plugins/dotnet/custom/CustomStringConverter.cs +++ b/generator-plugins/dotnet/custom/CustomStringConverter.cs @@ -30,7 +30,7 @@ public override void WriteJson(JsonWriter writer, T? value, JsonSerializer seria } else if (value is T t) { - writer.WriteValue(t); + writer.WriteValue(t.ToString()); } else { diff --git a/generator-plugins/dotnet/custom/LSPAnyConverter.cs b/generator-plugins/dotnet/custom/LSPAnyConverter.cs new file mode 100644 index 0000000..28781cb --- /dev/null +++ b/generator-plugins/dotnet/custom/LSPAnyConverter.cs @@ -0,0 +1,61 @@ +using Newtonsoft.Json; + +public class LSPAnyConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + return objectType == typeof(LSPAny); + } + + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + reader = reader ?? throw new ArgumentNullException(nameof(reader)); + switch (reader.TokenType) + { + case JsonToken.Null: + return null; + + case JsonToken.Integer: + return new LSPAny(serializer.Deserialize(reader)); + + case JsonToken.Float: + return new LSPAny(serializer.Deserialize(reader)); + + case JsonToken.Boolean: + return new LSPAny(serializer.Deserialize(reader)); + + case JsonToken.String: + return new LSPAny(serializer.Deserialize(reader)); + + case JsonToken.StartArray: + List? l = serializer.Deserialize>(reader); + if (l == null) + { + return null; + } + return new LSPAny(new LSPArray(l)); + + case JsonToken.StartObject: + Dictionary? o = serializer.Deserialize>(reader); + if (o == null) + { + return null; + } + return new LSPAny(new LSPObject(o)); + } + + throw new JsonSerializationException($"Unexpected token type '{reader.TokenType}' while deserializing '{objectType.Name}'."); + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + } + else + { + serializer.Serialize(writer, ((LSPAny)value).Value); + } + } +} \ No newline at end of file diff --git a/generator-plugins/dotnet/custom/OrTypeArrayConverter.cs b/generator-plugins/dotnet/custom/OrTypeArrayConverter.cs new file mode 100644 index 0000000..6d7780a --- /dev/null +++ b/generator-plugins/dotnet/custom/OrTypeArrayConverter.cs @@ -0,0 +1,143 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; + + +public class OrTypeArrayConverter : JsonConverter[]> +{ + private OrTypeConverter _converter; + + public OrTypeArrayConverter() + { + _converter = new OrTypeConverter(); + } + + public override OrType[] ReadJson(JsonReader reader, Type objectType, OrType[]? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + JArray array = JArray.Load(reader); + var result = new OrType[array.Count]; + + for (int i = 0; i < array.Count; i++) + { + result[i] = (OrType)_converter.ReadJson(array[i].CreateReader(), typeof(OrType), null, serializer); + } + + return result; + } + + public override void WriteJson(JsonWriter writer, OrType[] value, JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + } + else + { + writer.WriteStartArray(); + + foreach (var item in value) + { + _converter.WriteJson(writer, item, serializer); + } + + writer.WriteEndArray(); + } + } +} +public class OrTypeArrayConverter : JsonConverter[]> +{ + private OrTypeConverter _converter; + + public OrTypeArrayConverter() + { + _converter = new OrTypeConverter(); + } + + public override OrType[] ReadJson(JsonReader reader, Type objectType, OrType[]? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + JArray array = JArray.Load(reader); + var result = new OrType[array.Count]; + + for (int i = 0; i < array.Count; i++) + { + result[i] = (OrType)_converter.ReadJson(array[i].CreateReader(), typeof(OrType), null, serializer); + } + + return result; + } + + public override void WriteJson(JsonWriter writer, OrType[] value, JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + } + else + { + writer.WriteStartArray(); + + foreach (var item in value) + { + _converter.WriteJson(writer, item, serializer); + } + + writer.WriteEndArray(); + } + } +} +public class OrTypeArrayConverter : JsonConverter[]> +{ + private OrTypeConverter _converter; + + public OrTypeArrayConverter() + { + _converter = new OrTypeConverter(); + } + + public override OrType[] ReadJson(JsonReader reader, Type objectType, OrType[]? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + JArray array = JArray.Load(reader); + var result = new OrType[array.Count]; + + for (int i = 0; i < array.Count; i++) + { + result[i] = (OrType)_converter.ReadJson(array[i].CreateReader(), typeof(OrType), null, serializer); + } + + return result; + } + + public override void WriteJson(JsonWriter writer, OrType[] value, JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + } + else + { + writer.WriteStartArray(); + + foreach (var item in value) + { + _converter.WriteJson(writer, item, serializer); + } + + writer.WriteEndArray(); + } + } +} \ No newline at end of file diff --git a/generator-plugins/dotnet/custom/OrTypeConverter.cs b/generator-plugins/dotnet/custom/OrTypeConverter.cs index 38cffae..3053d6d 100644 --- a/generator-plugins/dotnet/custom/OrTypeConverter.cs +++ b/generator-plugins/dotnet/custom/OrTypeConverter.cs @@ -30,13 +30,7 @@ public class OrTypeConverter : JsonConverter> return ReadStringToken(reader, serializer, types); } - JToken? token = JToken.ReadFrom(reader); - while (token is not JObject) - { - token = JToken.ReadFrom(reader); - } - - return OrTypeConverter.ReadObjectToken(token, serializer, types); + return OrTypeConverter.ReadObjectToken(JToken.Load(reader), serializer, types); } private static OrType ReadIntegerToken(JsonReader reader, JsonSerializer serializer, Type[] types) @@ -110,7 +104,7 @@ private static OrType ReadStringToken(JsonReader reader, JsonSerializer se { return new OrType((T)(object)str); } - if (typeof(T) == typeof(string)) + if (typeof(U) == typeof(string)) { return new OrType((U)(object)str); } @@ -124,14 +118,30 @@ private static OrType ReadObjectToken(JToken token, JsonSerializer seriali { try { - object? value = token.ToObject(type, serializer); - if (value is T t) + object? value = null; + if (token.Type == JTokenType.Array && type == typeof((uint, uint))) { - return new OrType(t); + uint[]? o = token.ToObject(serializer); + if (o != null) + { + value = (o[0], o[1]); + } } - if (value is U u) + else { - return new OrType(u); + value = token.ToObject(type, serializer); + } + + if (value != null) + { + if (value is T t) + { + return new OrType(t); + } + if (value is U u) + { + return new OrType(u); + } } } catch @@ -151,9 +161,14 @@ public override void WriteJson(JsonWriter writer, OrType? value, JsonSeria { writer.WriteNull(); } + else if (value?.Value?.GetType() == typeof((uint, uint))) + { + ValueTuple o = (ValueTuple)(value.Value); + serializer.Serialize(writer, new uint[] { o.Item1, o.Item2 }); + } else { - serializer.Serialize(writer, value.Value); + serializer.Serialize(writer, value?.Value); } } } @@ -186,13 +201,7 @@ public class OrTypeConverter : JsonConverter> return ReadStringToken(reader, serializer, types); } - JToken? token = JToken.ReadFrom(reader); - while (token is not JObject) - { - token = JToken.ReadFrom(reader); - } - - return OrTypeConverter.ReadObjectToken(token, serializer, types); + return OrTypeConverter.ReadObjectToken(JToken.Load(reader), serializer, types); } private static OrType ReadIntegerToken(JsonReader reader, JsonSerializer serializer, Type[] types) @@ -300,18 +309,34 @@ private static OrType ReadObjectToken(JToken token, JsonSerializer seri { try { - object? value = token.ToObject(type, serializer); - if (value is T t) + object? value = null; + if (token.Type == JTokenType.Array && type == typeof((uint, uint))) { - return new OrType(t); + uint[]? o = token.ToObject(serializer); + if (o != null) + { + value = (o[0], o[1]); + } } - if (value is U u) + else { - return new OrType(u); + value = token.ToObject(type, serializer); } - if (value is V v) + + if (value != null) { - return new OrType(v); + if (value is T t) + { + return new OrType(t); + } + if (value is U u) + { + return new OrType(u); + } + if (value is V v) + { + return new OrType(v); + } } } catch @@ -329,6 +354,11 @@ public override void WriteJson(JsonWriter writer, OrType? value, JsonSe { writer.WriteNull(); } + else if (value?.Value?.GetType() == typeof((uint, uint))) + { + ValueTuple o = (ValueTuple)(value.Value); + serializer.Serialize(writer, new uint[] { o.Item1, o.Item2 }); + } else { serializer.Serialize(writer, value.Value); @@ -364,13 +394,7 @@ public class OrTypeConverter : JsonConverter> return ReadStringToken(reader, serializer, types); } - JToken? token = JToken.ReadFrom(reader); - while (token is not JObject) - { - token = JToken.ReadFrom(reader); - } - - return OrTypeConverter.ReadObjectToken(token, serializer, types); + return OrTypeConverter.ReadObjectToken(JToken.Load(reader), serializer, types); } private static OrType ReadIntegerToken(JsonReader reader, JsonSerializer serializer, Type[] types) @@ -498,23 +522,40 @@ private static OrType ReadObjectToken(JToken token, JsonSerializer s { try { - object? value = token.ToObject(type, serializer); - if (value is T t) + object? value = null; + if (token.Type == JTokenType.Array && type == typeof((uint, uint))) { - return new OrType(t); + uint[]? o = token.ToObject(serializer); + if (o != null) + { + value = (o[0], o[1]); + } } - if (value is U u) + else { - return new OrType(u); + value = token.ToObject(type, serializer); } - if (value is V v) - { - return new OrType(v); - } - if (value is W w) + + if (value != null) { - return new OrType(w); + if (value is T t) + { + return new OrType(t); + } + if (value is U u) + { + return new OrType(u); + } + if (value is V v) + { + return new OrType(v); + } + if (value is W w) + { + return new OrType(w); + } } + } catch { @@ -531,6 +572,11 @@ public override void WriteJson(JsonWriter writer, OrType? value, Jso { writer.WriteNull(); } + else if (value?.Value?.GetType() == typeof((uint, uint))) + { + ValueTuple o = (ValueTuple)(value.Value); + serializer.Serialize(writer, new uint[] { o.Item1, o.Item2 }); + } else { serializer.Serialize(writer, value.Value); diff --git a/generator-plugins/dotnet/dotnet_classes.py b/generator-plugins/dotnet/dotnet_classes.py index e6730ec..e7113f7 100644 --- a/generator-plugins/dotnet/dotnet_classes.py +++ b/generator-plugins/dotnet/dotnet_classes.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import re from typing import Any, Dict, List, Optional, Tuple, Union import cattrs @@ -23,6 +24,8 @@ to_upper_camel_case, ) +CONVERTER_RE = re.compile(r"OrType<(?P.*)>\[\]") + def _get_enum(name: str, spec: model.LSPModel) -> Optional[model.Enum]: for enum in spec.enumerations: @@ -129,12 +132,7 @@ def get_type_name( return name -def get_converter( - type_def: model.LSP_TYPE_SPEC, - types: TypeData, - spec: model.LSPModel, - name_context: Optional[str] = None, -) -> Optional[str]: +def get_converter(type_def: model.LSP_TYPE_SPEC, type_name: str) -> Optional[str]: if type_def.kind == "base" and type_def.name in ["DocumentUri", "URI"]: return "[JsonConverter(typeof(CustomStringConverter))]" elif type_def.kind == "reference" and type_def.name in [ @@ -144,13 +142,14 @@ def get_converter( return f"[JsonConverter(typeof(CustomStringConverter<{type_def.name}>))]" elif type_def.kind == "reference" and type_def.name == "DocumentSelector": return "[JsonConverter(typeof(DocumentSelectorConverter))]" - elif type_def.kind == "or": - subset = filter_null_base_type(type_def.items) - if len(subset) >= 2: - type_names = ", ".join( - get_type_name(item, types, spec, name_context) for item in subset - ) - return f"[JsonConverter(typeof(OrTypeConverter<{type_names}>))]" + elif type_def.kind == "or" and type_name.startswith("OrType<"): + converter = type_name.replace("OrType<", "OrTypeConverter<") + return f"[JsonConverter(typeof({converter}))]" + elif type_def.kind == "array" and type_name.startswith("OrType<"): + matches = CONVERTER_RE.match(type_name).groupdict() + if "parts" in matches: + converter = f"OrTypeArrayConverter<{matches['parts']}>" + return f"[JsonConverter(typeof({converter}))]" return None @@ -168,9 +167,7 @@ def generate_property( type_name = get_type_name( prop_def.type, types, spec, f"{class_name}_{prop_def.name}" ) - converter = get_converter( - prop_def.type, types, spec, f"{class_name}_{prop_def.name}" - ) + converter = get_converter(prop_def.type, type_name) special_optional = prop_def.type.kind == "or" and has_null_base_type( prop_def.type.items ) @@ -416,6 +413,56 @@ def generate_type_alias_constructor( return constructor +def generate_type_alias_converter( + type_def: model.TypeAlias, spec: model.LSPModel, types: TypeData +) -> None: + assert type_def.type.kind == "or" + subset_types = [ + get_type_name(i, types, spec, type_def.name) + for i in filter_null_base_type(type_def.type.items) + ] + converter = f"{type_def.name}Converter" + or_type_converter = f"OrTypeConverter<{','.join(subset_types)}>" + or_type = f"OrType<{','.join(subset_types)}>" + code = [ + f"public class {converter} : JsonConverter<{type_def.name}>", + "{", + f"private {or_type_converter} _orType;", + f"public {converter}()", + "{", + f"_orType = new {or_type_converter}();", + "}", + f"public override {type_def.name}? ReadJson(JsonReader reader, Type objectType, {type_def.name}? existingValue, bool hasExistingValue, JsonSerializer serializer)", + "{", + f"var o = _orType.ReadJson(reader, objectType, existingValue, serializer);", + f"if (o is {or_type} orType)", + "{", + ] + for t in subset_types: + code += [ + f"if (orType.Value.GetType() == typeof({t}))", + "{", + f"return new {type_def.name}(({t})orType.Value);", + "}", + ] + code += [ + "}", + "throw new JsonSerializationException($\"Unexpected token type '{{orType.GetType()}}'.\");", + "}", + f"public override void WriteJson(JsonWriter writer, {type_def.name}? value, JsonSerializer serializer)", + "{", + "_orType.WriteJson(writer, value, serializer);", + "}", + "}", + ] + + code = namespace_wrapper(NAMESPACE, get_usings(["JsonConverter"]), code) + + ref = model.Structure(**{"name": converter, "properties": []}) + types.add_type_info(ref, converter, code) + return converter + + def generate_class_from_type_alias( type_def: model.TypeAlias, spec: model.LSPModel, types: TypeData ) -> None: @@ -426,7 +473,8 @@ def generate_class_from_type_alias( type_name = get_type_name(type_def.type, types, spec, type_def.name) class_attributes = [] if type_def.type.kind == "or": - class_attributes += [f"[JsonConverter(typeof({type_name}))]"] + converter = generate_type_alias_converter(type_def, spec, types) + class_attributes += [f"[JsonConverter(typeof({converter}))]"] usings.append("JsonConverter") inner = generate_type_alias_constructor(type_def, spec, types) @@ -672,8 +720,6 @@ def generate_request_notification_methods(spec: model.LSPModel, types: TypeData) def get_message_template( obj: Union[model.Request, model.Notification], - spec: model.LSPModel, - types: TypeData, is_request: bool, ) -> model.Structure: text = "Request" if is_request else "Notification" @@ -709,10 +755,7 @@ def get_message_template( properties.append( { "name": "params", - "type": { - "kind": "reference", - "name": get_type_name(obj.params, types, spec), - }, + "type": cattrs.unstructure(obj.params), "documentation": f"The {text} parameters.", } ) @@ -776,13 +819,16 @@ def generate_all_classes(spec: model.LSPModel, types: TypeData): generate_request_notification_methods(spec, types) for request in spec.requests: - struct = get_message_template(request, spec, types, is_request=True) - params = struct.properties[3] + struct = get_message_template(request, is_request=True) generate_class_from_struct( struct, spec, types, - f"IRequest<{params.type.name}>", + ( + f"IRequest<{get_type_name(request.params, types, spec)}>" + if request.params + else "IRequest" + ), ) registration_options = get_registration_options_template(request, spec, types) if registration_options: @@ -793,13 +839,16 @@ def generate_all_classes(spec: model.LSPModel, types: TypeData): ) for notification in spec.notifications: - struct = get_message_template(notification, spec, types, is_request=False) - params = struct.properties[2] + struct = get_message_template(notification, is_request=False) generate_class_from_struct( struct, spec, types, - f"INotification<{params.type.name}>", + ( + f"INotification<{get_type_name(notification.params, types, spec)}>" + if notification.params + else "INotification" + ), ) registration_options = get_registration_options_template( notification, spec, types diff --git a/generator-plugins/dotnet/dotnet_enums.py b/generator-plugins/dotnet/dotnet_enums.py index 0e76ded..c5e7a6c 100644 --- a/generator-plugins/dotnet/dotnet_enums.py +++ b/generator-plugins/dotnet/dotnet_enums.py @@ -28,9 +28,13 @@ def _get_enum_doc(enum: Union[model.Enum, model.EnumItem]) -> List[str]: def generate_enum(enum: model.Enum) -> List[str]: use_enum_member = all(isinstance(item.value, str) for item in enum.values) - imports = ["using System.Runtime.Serialization;", ""] + imports = ["using System.Runtime.Serialization;"] + if use_enum_member: + imports += ["using Newtonsoft.Json;", "using Newtonsoft.Json.Converters;"] lines = _get_enum_doc(enum) + if use_enum_member: + lines += ["[JsonConverter(typeof(StringEnumConverter))]"] lines += [f"public enum {enum.name}", "{"] for item in enum.values: diff --git a/generator-plugins/dotnet/dotnet_special_classes.py b/generator-plugins/dotnet/dotnet_special_classes.py index bbc047f..0300cbb 100644 --- a/generator-plugins/dotnet/dotnet_special_classes.py +++ b/generator-plugins/dotnet/dotnet_special_classes.py @@ -16,6 +16,7 @@ "ChangeAnnotationIdentifier", "Pattern", "DocumentSelector", + "InitializedParams", ] @@ -38,20 +39,50 @@ def generate_special_class( if type_def.name == "LSPObject": lines = namespace_wrapper( NAMESPACE, - get_usings(["Dictionary", "DataContract"]), - class_wrapper(type_def, [], "Dictionary"), + get_usings(["Dictionary", "DataContract", "JsonConverter"]), + class_wrapper( + type_def, + ["public LSPObject(Dictionary value):base(value){}"], + "Dictionary", + ["[JsonConverter(typeof(CustomObjectConverter))]"], + ), + ) + if type_def.name == "InitializedParams": + lines = namespace_wrapper( + NAMESPACE, + get_usings(["Dictionary", "DataContract", "JsonConverter"]), + class_wrapper( + type_def, + [ + "public InitializedParams(Dictionary value):base(value){}" + ], + "Dictionary", + ["[JsonConverter(typeof(CustomObjectConverter))]"], + ), ) if type_def.name == "LSPAny": lines = namespace_wrapper( NAMESPACE, - get_usings(["DataContract"]), - class_wrapper(type_def, [], "object"), + get_usings(["DataContract", "JsonConverter"]), + class_wrapper( + type_def, + [ + "public LSPAny(object? value){this.Value = value;}", + "public object? Value { get; set; }", + ], + "object", + ["[JsonConverter(typeof(LSPAnyConverter))]"], + ), ) if type_def.name == "LSPArray": lines = namespace_wrapper( NAMESPACE, get_usings(["DataContract", "List"]), - class_wrapper(type_def, [], "List"), + class_wrapper( + type_def, + ["public LSPArray(List value):base(value){}"], + "List", + ), ) if type_def.name == "Pattern": @@ -60,6 +91,7 @@ def generate_special_class( "public Pattern(string value){pattern = value;}", "public static implicit operator Pattern(string value) => new Pattern(value);", "public static implicit operator string(Pattern pattern) => pattern.pattern;", + "public override string ToString() => pattern;", ] lines = namespace_wrapper( NAMESPACE, @@ -78,6 +110,7 @@ def generate_special_class( "public ChangeAnnotationIdentifier(string value){identifier = value;}", "public static implicit operator ChangeAnnotationIdentifier(string value) => new ChangeAnnotationIdentifier(value);", "public static implicit operator string(ChangeAnnotationIdentifier identifier) => identifier.identifier;", + "public override string ToString() => identifier;", ] lines = namespace_wrapper( NAMESPACE, diff --git a/generator-plugins/testdata/testdata_generator.py b/generator-plugins/testdata/testdata_generator.py index 907b68f..3b3d1f0 100644 --- a/generator-plugins/testdata/testdata_generator.py +++ b/generator-plugins/testdata/testdata_generator.py @@ -176,7 +176,7 @@ def get_all_extends(struct_def: model.Structure, spec) -> List[model.Structure]: return extends -def get_all_properties(struct: model.Structure, spec) -> List[model.Structure]: +def get_all_properties(struct: model.Structure, spec) -> List[model.Property]: properties = [] for prop in struct.properties: properties.append(prop) @@ -196,6 +196,14 @@ def get_all_properties(struct: model.Structure, spec) -> List[model.Structure]: return properties +def generate_for_property( + prop: model.Property, spec: model.LSPModel, visited: List[str] +): + if prop.optional: + yield (True, Ignore()) + yield from generate_for_type(prop.type, spec, visited) + + def generate_for_reference( refname: str, spec: model.LSPModel, @@ -212,7 +220,7 @@ def generate_for_reference( if properties: names = [prop.name for prop in properties] value_variants = [ - list(generate_for_type(prop.type, spec, visited)) for prop in properties + list(generate_for_property(prop, spec, visited)) for prop in properties ] products = zip(*extend_all(value_variants)) @@ -229,7 +237,13 @@ def generate_for_reference( yield (True, {"lspExtension": "some value"}) yield (True, dict()) elif alias: - yield from generate_for_type(alias.type, spec, visited) + if refname in ["LSPObject", "LSPAny", "LSPArray"]: + yield from ( + (True, value) + for _, value in generate_for_type(alias.type, spec, visited) + ) + else: + yield from generate_for_type(alias.type, spec, visited) elif enum: value = enum.values[0].value yield (True, value) @@ -247,7 +261,7 @@ def generate_for_or( visited: List[str], ): if has_null_base_type(type_defs): - yield (True, Ignore()) + yield (True, None) subset = filter_null_base_type(type_defs) generated = [ @@ -306,7 +320,7 @@ def generate_for_type( visited: List[str], ): if type_def is None: - yield (True, Ignore()) + yield (True, None) elif type_def.kind == "base": yield from generate_for_base(type_def.name) elif type_def.kind == "array": diff --git a/generator-plugins/testdata/testdata_utils.py b/generator-plugins/testdata/testdata_utils.py index 6b7f56d..a0d60fa 100644 --- a/generator-plugins/testdata/testdata_utils.py +++ b/generator-plugins/testdata/testdata_utils.py @@ -14,6 +14,7 @@ def generate_from_spec(spec: model.LSPModel, output_dir: str) -> None: """Generate the code for the given spec.""" + cleanup(output_dir) output = pathlib.Path(output_dir) # key is the relative path to the file, value is the content code: Dict[str, str] = generate(spec, logger) @@ -21,3 +22,10 @@ def generate_from_spec(spec: model.LSPModel, output_dir: str) -> None: # print file size file = output / file_name file.write_text(code[file_name], encoding="utf-8") + + +def cleanup(output_dir: str) -> None: + """Cleanup the generated C# files.""" + output = pathlib.Path(output_dir) + for file in output.glob("*.json"): + file.unlink() diff --git a/tests/dotnet/lsprotocol_tests/LSPTests.cs b/tests/dotnet/lsprotocol_tests/LSPTests.cs index 4b90c09..132f923 100644 --- a/tests/dotnet/lsprotocol_tests/LSPTests.cs +++ b/tests/dotnet/lsprotocol_tests/LSPTests.cs @@ -8,7 +8,16 @@ public class LSPTests { public static IEnumerable JsonTestData() { - string folderPath = "C:\\GIT\\LSP\\lsprotocol\\packages\\testdata"; + string folderPath; + // Read test data path from environment variable + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("LSP_TEST_DATA_PATH"))) + { + folderPath = Environment.GetEnvironmentVariable("LSP_TEST_DATA_PATH"); + } + else + { + throw new Exception("LSP_TEST_DATA_PATH environment variable not set"); + } string[] jsonFiles = Directory.GetFiles(folderPath, "*.json"); foreach (string filePath in jsonFiles) @@ -41,16 +50,66 @@ private static void RunTest(bool valid, string data, Type type) { if (valid) { - object? deserializedObject = JsonConvert.DeserializeObject(data, type); - string newJson = JsonConvert.SerializeObject(deserializedObject); + var settings = new JsonSerializerSettings + { + MissingMemberHandling = MissingMemberHandling.Error + }; + object? deserializedObject = JsonConvert.DeserializeObject(data, type, settings); + string newJson = JsonConvert.SerializeObject(deserializedObject, settings); JToken token1 = JToken.Parse(data); JToken token2 = JToken.Parse(newJson); + RemoveNullProperties(token1); + RemoveNullProperties(token2); Assert.True(JToken.DeepEquals(token1, token2)); } else { - Assert.Throws(() => JsonConvert.DeserializeObject(data, type)); + try + { + JsonConvert.DeserializeObject(data, type); + // Explicitly fail the test + Assert.True(false, "Should have thrown an exception."); + } + catch + { + // Worked as expected. + } + } + } + + private static void RemoveNullProperties(JToken token) + { + if (token.Type == JTokenType.Object) + { + var obj = (JObject)token; + + var propertiesToRemove = obj.Properties() + .Where(p => p.Value.Type == JTokenType.Null) + .ToList(); + + foreach (var property in propertiesToRemove) + { + property.Remove(); + } + + foreach (var property in obj.Properties()) + { + RemoveNullProperties(property.Value); + } + } + else if (token.Type == JTokenType.Array) + { + var array = (JArray)token; + + for (int i = array.Count - 1; i >= 0; i--) + { + RemoveNullProperties(array[i]); + if (array[i].Type == JTokenType.Null) + { + array.RemoveAt(i); + } + } } } } \ No newline at end of file