diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ea3a611..051cace 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,32 +1,46 @@ version: 2 updates: - - package-ecosystem: "github-actions" + - package-ecosystem: 'github-actions' directory: / schedule: interval: monthly labels: - - "no-changelog" + - 'no-changelog' - - package-ecosystem: "pip" + - package-ecosystem: 'github-actions' + directory: .github/actions/lint + schedule: + interval: monthly + labels: + - 'no-changelog' + + - package-ecosystem: 'github-actions' + directory: .github/actions/build-package + schedule: + interval: monthly + labels: + - 'no-changelog' + + - package-ecosystem: 'pip' directory: /generator schedule: interval: daily labels: - - "no-changelog" + - 'no-changelog' - - package-ecosystem: "pip" + - package-ecosystem: 'pip' directory: /tests schedule: interval: daily labels: - - "no-changelog" + - 'no-changelog' - - package-ecosystem: "pip" + - package-ecosystem: 'pip' directory: / schedule: interval: daily labels: - - "debt" + - 'debt' commit-message: - include: "scope" - prefix: "pip" + include: 'scope' + prefix: 'pip' diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 547fcaa..b5e8b4b 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -80,6 +80,10 @@ jobs: - name: Generate C# Code run: python -m generator --plugin dotnet + - name: Dotnet Build Tests + run: dotnet build tests/dotnet/lsprotocol_tests/lsprotocol_tests.csproj + shell: bash + - name: Dotnet Run Tests run: dotnet test tests/dotnet/lsprotocol_tests/lsprotocol_tests.csproj shell: bash diff --git a/generator/plugins/dotnet/custom/IResponse.cs b/generator/plugins/dotnet/custom/IResponse.cs index 9d34227..01ecf3e 100644 --- a/generator/plugins/dotnet/custom/IResponse.cs +++ b/generator/plugins/dotnet/custom/IResponse.cs @@ -1,9 +1,9 @@ public interface IResponse : IMessage { - OrType Id { get; } + OrType Id { get; } - TResponse Result { get; } + TResponse? Result { get; } - IResponseError? Error { get; } + ResponseError? Error { get; } } \ No newline at end of file diff --git a/generator/plugins/dotnet/custom/IResponseError.cs b/generator/plugins/dotnet/custom/IResponseError.cs deleted file mode 100644 index c21cf32..0000000 --- a/generator/plugins/dotnet/custom/IResponseError.cs +++ /dev/null @@ -1,9 +0,0 @@ -public interface IResponseError -{ - - int Code { get; } - - string Message { get; } - - LSPObject? Data { get; } -} \ No newline at end of file diff --git a/generator/plugins/dotnet/custom/LSPRequest.cs b/generator/plugins/dotnet/custom/LSPRequest.cs new file mode 100644 index 0000000..69e3f0d --- /dev/null +++ b/generator/plugins/dotnet/custom/LSPRequest.cs @@ -0,0 +1,22 @@ +using System; + +[AttributeUsage(AttributeTargets.Class)] +public class LSPRequestAttribute : Attribute +{ + public LSPRequestAttribute(string method, Type response) + { + Method = method; + Response = response; + } + + public LSPRequestAttribute(string method, Type response, Type partialResponse) + { + Method = method; + Response = response; + PartialResponse = partialResponse; + } + + public string Method { get; } + public Type Response { get; } + public Type? PartialResponse { get; } +} \ No newline at end of file diff --git a/generator/plugins/dotnet/custom/LSPResponse.cs b/generator/plugins/dotnet/custom/LSPResponse.cs new file mode 100644 index 0000000..4d2ca46 --- /dev/null +++ b/generator/plugins/dotnet/custom/LSPResponse.cs @@ -0,0 +1,13 @@ +using System; + +[AttributeUsage(AttributeTargets.Class)] +public class LSPResponseAttribute : Attribute +{ + public LSPResponseAttribute(Type request) + { + Request = request; + } + + + public Type Request { get; } +} \ No newline at end of file diff --git a/generator/plugins/dotnet/custom/OrTypeArrayConverter.cs b/generator/plugins/dotnet/custom/OrTypeArrayConverter.cs index 6d7780a..65bb36d 100644 --- a/generator/plugins/dotnet/custom/OrTypeArrayConverter.cs +++ b/generator/plugins/dotnet/custom/OrTypeArrayConverter.cs @@ -3,16 +3,16 @@ using System; -public class OrTypeArrayConverter : JsonConverter[]> +public class OrTypeArrayConverter : JsonConverter[]> { - private OrTypeConverter _converter; + private OrTypeConverter _converter; public OrTypeArrayConverter() { - _converter = new OrTypeConverter(); + _converter = new OrTypeConverter(); } - public override OrType[] ReadJson(JsonReader reader, Type objectType, OrType[]? existingValue, bool hasExistingValue, JsonSerializer serializer) + public override OrType[] ReadJson(JsonReader reader, Type objectType, OrType[]? existingValue, bool hasExistingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) { @@ -20,17 +20,17 @@ public override OrType[] ReadJson(JsonReader reader, Type objectType } JArray array = JArray.Load(reader); - var result = new OrType[array.Count]; + 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); + result[i] = (OrType)_converter.ReadJson(array[i].CreateReader(), typeof(OrType), null, serializer); } return result; } - public override void WriteJson(JsonWriter writer, OrType[] value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, OrType[]? value, JsonSerializer serializer) { if (value is null) { @@ -49,16 +49,16 @@ public override void WriteJson(JsonWriter writer, OrType[] value, Js } } } -public class OrTypeArrayConverter : JsonConverter[]> +public class OrTypeArrayConverter : JsonConverter[]> { - private OrTypeConverter _converter; + private OrTypeConverter _converter; public OrTypeArrayConverter() { - _converter = new OrTypeConverter(); + _converter = new OrTypeConverter(); } - public override OrType[] ReadJson(JsonReader reader, Type objectType, OrType[]? existingValue, bool hasExistingValue, JsonSerializer serializer) + public override OrType[] ReadJson(JsonReader reader, Type objectType, OrType[]? existingValue, bool hasExistingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) { @@ -66,17 +66,17 @@ public override OrType[] ReadJson(JsonReader reader, Type objectType, OrTy } JArray array = JArray.Load(reader); - var result = new OrType[array.Count]; + 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); + result[i] = (OrType)_converter.ReadJson(array[i].CreateReader(), typeof(OrType), null, serializer); } return result; } - public override void WriteJson(JsonWriter writer, OrType[] value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, OrType[]? value, JsonSerializer serializer) { if (value is null) { @@ -95,16 +95,18 @@ public override void WriteJson(JsonWriter writer, OrType[] value, JsonSeri } } } -public class OrTypeArrayConverter : JsonConverter[]> + + +public class OrTypeArrayConverter : JsonConverter[]> { - private OrTypeConverter _converter; + private OrTypeConverter _converter; public OrTypeArrayConverter() { - _converter = new OrTypeConverter(); + _converter = new OrTypeConverter(); } - public override OrType[] ReadJson(JsonReader reader, Type objectType, OrType[]? existingValue, bool hasExistingValue, JsonSerializer serializer) + public override OrType[] ReadJson(JsonReader reader, Type objectType, OrType[]? existingValue, bool hasExistingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) { @@ -112,17 +114,17 @@ public override OrType[] ReadJson(JsonReader reader, Type objectType, O } JArray array = JArray.Load(reader); - var result = new OrType[array.Count]; + 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); + result[i] = (OrType)_converter.ReadJson(array[i].CreateReader(), typeof(OrType), null, serializer); } return result; } - public override void WriteJson(JsonWriter writer, OrType[] value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, OrType[]? value, JsonSerializer serializer) { if (value is null) { diff --git a/generator/plugins/dotnet/custom/OrTypeConverter.cs b/generator/plugins/dotnet/custom/OrTypeConverter.cs index 3053d6d..ae3c12a 100644 --- a/generator/plugins/dotnet/custom/OrTypeConverter.cs +++ b/generator/plugins/dotnet/custom/OrTypeConverter.cs @@ -15,19 +15,21 @@ public class OrTypeConverter : JsonConverter> Type[] types = new Type[] { typeof(T), typeof(U) }; - switch (reader.TokenType) + if (reader.TokenType == JsonToken.Integer && (Validators.HasType(types, typeof(uint)) || Validators.HasType(types, typeof(int)))) { - case JsonToken.Integer: - return ReadIntegerToken(reader, serializer, types); - - case JsonToken.Float: - return ReadFloatToken(reader, serializer, types); - - case JsonToken.Boolean: - return ReadBooleanToken(reader, serializer, types); - - case JsonToken.String: - return ReadStringToken(reader, serializer, types); + return ReadIntegerToken(reader, serializer, types); + } + if (reader.TokenType == JsonToken.Float && Validators.HasType(types, typeof(float))) + { + return ReadFloatToken(reader, serializer, types); + } + if (reader.TokenType == JsonToken.Boolean && Validators.HasType(types, typeof(bool))) + { + return ReadBooleanToken(reader, serializer, types); + } + if (reader.TokenType == JsonToken.String && Validators.HasType(types, typeof(string))) + { + return ReadStringToken(reader, serializer, types); } return OrTypeConverter.ReadObjectToken(JToken.Load(reader), serializer, types); @@ -63,57 +65,49 @@ private static OrType ReadIntegerToken(JsonReader reader, JsonSerializer s private static OrType ReadFloatToken(JsonReader reader, JsonSerializer serializer, Type[] types) { - if (Validators.HasType(types, typeof(float))) + float real = serializer.Deserialize(reader); + if (typeof(T) == typeof(float)) { - float real = serializer.Deserialize(reader); - if (typeof(T) == typeof(float)) - { - return new OrType((T)(object)real); - } - if (typeof(U) == typeof(float)) - { - return new OrType((U)(object)real); - } + return new OrType((T)(object)real); + } + if (typeof(U) == typeof(float)) + { + return new OrType((U)(object)real); } throw new InvalidOperationException("Invalid token type for float"); } private static OrType ReadBooleanToken(JsonReader reader, JsonSerializer serializer, Type[] types) { - if (Validators.HasType(types, typeof(bool))) + bool boolean = serializer.Deserialize(reader); + if (typeof(T) == typeof(bool)) { - bool boolean = serializer.Deserialize(reader); - if (typeof(T) == typeof(bool)) - { - return new OrType((T)(object)boolean); - } - if (typeof(U) == typeof(bool)) - { - return new OrType((U)(object)boolean); - } + return new OrType((T)(object)boolean); + } + if (typeof(U) == typeof(bool)) + { + return new OrType((U)(object)boolean); } throw new InvalidOperationException("Invalid token type for boolean"); } private static OrType ReadStringToken(JsonReader reader, JsonSerializer serializer, Type[] types) { - if (Validators.HasType(types, typeof(string))) + string str = serializer.Deserialize(reader); + if (typeof(T) == typeof(string)) { - string str = serializer.Deserialize(reader); - if (typeof(T) == typeof(string)) - { - return new OrType((T)(object)str); - } - if (typeof(U) == typeof(string)) - { - return new OrType((U)(object)str); - } + return new OrType((T)(object)str); + } + if (typeof(U) == typeof(string)) + { + return new OrType((U)(object)str); } throw new InvalidOperationException("Invalid token type for string"); } private static OrType ReadObjectToken(JToken token, JsonSerializer serializer, Type[] types) { + var exceptions = new List(); foreach (Type type in types) { try @@ -144,13 +138,14 @@ private static OrType ReadObjectToken(JToken token, JsonSerializer seriali } } } - catch + catch (Exception ex) { + exceptions.Add(ex); continue; } } - throw new ArgumentException(nameof(token)); + throw new JsonSerializationException("Unable to deserialize object", new AggregateException(exceptions)); } @@ -186,19 +181,21 @@ public class OrTypeConverter : JsonConverter> Type[] types = new Type[] { typeof(T), typeof(U), typeof(V) }; - switch (reader.TokenType) + if (reader.TokenType == JsonToken.Integer && (Validators.HasType(types, typeof(uint)) || Validators.HasType(types, typeof(int)))) { - case JsonToken.Integer: - return ReadIntegerToken(reader, serializer, types); - - case JsonToken.Float: - return ReadFloatToken(reader, serializer, types); - - case JsonToken.Boolean: - return ReadBooleanToken(reader, serializer, types); - - case JsonToken.String: - return ReadStringToken(reader, serializer, types); + return ReadIntegerToken(reader, serializer, types); + } + if (reader.TokenType == JsonToken.Float && Validators.HasType(types, typeof(float))) + { + return ReadFloatToken(reader, serializer, types); + } + if (reader.TokenType == JsonToken.Boolean && Validators.HasType(types, typeof(bool))) + { + return ReadBooleanToken(reader, serializer, types); + } + if (reader.TokenType == JsonToken.String && Validators.HasType(types, typeof(string))) + { + return ReadStringToken(reader, serializer, types); } return OrTypeConverter.ReadObjectToken(JToken.Load(reader), serializer, types); @@ -242,69 +239,61 @@ private static OrType ReadIntegerToken(JsonReader reader, JsonSerialize private static OrType ReadFloatToken(JsonReader reader, JsonSerializer serializer, Type[] types) { - if (Validators.HasType(types, typeof(float))) + float real = serializer.Deserialize(reader); + if (typeof(T) == typeof(float)) { - float real = serializer.Deserialize(reader); - if (typeof(T) == typeof(float)) - { - return new OrType((T)(object)real); - } - if (typeof(U) == typeof(float)) - { - return new OrType((U)(object)real); - } - if (typeof(V) == typeof(float)) - { - return new OrType((V)(object)real); - } + return new OrType((T)(object)real); + } + if (typeof(U) == typeof(float)) + { + return new OrType((U)(object)real); + } + if (typeof(V) == typeof(float)) + { + return new OrType((V)(object)real); } throw new InvalidOperationException("Invalid token type for float"); } private static OrType ReadBooleanToken(JsonReader reader, JsonSerializer serializer, Type[] types) { - if (Validators.HasType(types, typeof(bool))) + bool boolean = serializer.Deserialize(reader); + if (typeof(T) == typeof(bool)) { - bool boolean = serializer.Deserialize(reader); - if (typeof(T) == typeof(bool)) - { - return new OrType((T)(object)boolean); - } - if (typeof(U) == typeof(bool)) - { - return new OrType((U)(object)boolean); - } - if (typeof(V) == typeof(bool)) - { - return new OrType((V)(object)boolean); - } + return new OrType((T)(object)boolean); + } + if (typeof(U) == typeof(bool)) + { + return new OrType((U)(object)boolean); + } + if (typeof(V) == typeof(bool)) + { + return new OrType((V)(object)boolean); } throw new InvalidOperationException("Invalid token type for boolean"); } private static OrType ReadStringToken(JsonReader reader, JsonSerializer serializer, Type[] types) { - if (Validators.HasType(types, typeof(string))) + string str = serializer.Deserialize(reader); + if (typeof(T) == typeof(string)) { - string str = serializer.Deserialize(reader); - if (typeof(T) == typeof(string)) - { - return new OrType((T)(object)str); - } - if (typeof(U) == typeof(string)) - { - return new OrType((U)(object)str); - } - if (typeof(V) == typeof(string)) - { - return new OrType((V)(object)str); - } + return new OrType((T)(object)str); + } + if (typeof(U) == typeof(string)) + { + return new OrType((U)(object)str); + } + if (typeof(V) == typeof(string)) + { + return new OrType((V)(object)str); } throw new InvalidOperationException("Invalid token type for string"); } private static OrType ReadObjectToken(JToken token, JsonSerializer serializer, Type[] types) { + var exceptions = new List(); foreach (Type type in types) { try @@ -339,13 +328,14 @@ private static OrType ReadObjectToken(JToken token, JsonSerializer seri } } } - catch + catch (Exception ex) { + exceptions.Add(ex); continue; } } - throw new ArgumentException(nameof(token)); + throw new JsonSerializationException("Unable to deserialize object", new AggregateException(exceptions)); } public override void WriteJson(JsonWriter writer, OrType? value, JsonSerializer serializer) @@ -379,19 +369,21 @@ public class OrTypeConverter : JsonConverter> Type[] types = new Type[] { typeof(T), typeof(U), typeof(V), typeof(W) }; - switch (reader.TokenType) + if (reader.TokenType == JsonToken.Integer && (Validators.HasType(types, typeof(uint)) || Validators.HasType(types, typeof(int)))) { - case JsonToken.Integer: - return ReadIntegerToken(reader, serializer, types); - - case JsonToken.Float: - return ReadFloatToken(reader, serializer, types); - - case JsonToken.Boolean: - return ReadBooleanToken(reader, serializer, types); - - case JsonToken.String: - return ReadStringToken(reader, serializer, types); + return ReadIntegerToken(reader, serializer, types); + } + if (reader.TokenType == JsonToken.Float && Validators.HasType(types, typeof(float))) + { + return ReadFloatToken(reader, serializer, types); + } + if (reader.TokenType == JsonToken.Boolean && Validators.HasType(types, typeof(bool))) + { + return ReadBooleanToken(reader, serializer, types); + } + if (reader.TokenType == JsonToken.String && Validators.HasType(types, typeof(string))) + { + return ReadStringToken(reader, serializer, types); } return OrTypeConverter.ReadObjectToken(JToken.Load(reader), serializer, types); @@ -443,81 +435,73 @@ private static OrType ReadIntegerToken(JsonReader reader, JsonSerial private static OrType ReadFloatToken(JsonReader reader, JsonSerializer serializer, Type[] types) { - if (Validators.HasType(types, typeof(float))) + float real = serializer.Deserialize(reader); + if (typeof(T) == typeof(float)) { - float real = serializer.Deserialize(reader); - if (typeof(T) == typeof(float)) - { - return new OrType((T)(object)real); - } - if (typeof(U) == typeof(float)) - { - return new OrType((U)(object)real); - } - if (typeof(V) == typeof(float)) - { - return new OrType((V)(object)real); - } - if (typeof(W) == typeof(float)) - { - return new OrType((W)(object)real); - } + return new OrType((T)(object)real); + } + if (typeof(U) == typeof(float)) + { + return new OrType((U)(object)real); + } + if (typeof(V) == typeof(float)) + { + return new OrType((V)(object)real); + } + if (typeof(W) == typeof(float)) + { + return new OrType((W)(object)real); } throw new InvalidOperationException("Invalid token type for float"); } private static OrType ReadBooleanToken(JsonReader reader, JsonSerializer serializer, Type[] types) { - if (Validators.HasType(types, typeof(bool))) + bool boolean = serializer.Deserialize(reader); + if (typeof(T) == typeof(bool)) { - bool boolean = serializer.Deserialize(reader); - if (typeof(T) == typeof(bool)) - { - return new OrType((T)(object)boolean); - } - if (typeof(U) == typeof(bool)) - { - return new OrType((U)(object)boolean); - } - if (typeof(V) == typeof(bool)) - { - return new OrType((V)(object)boolean); - } - if (typeof(W) == typeof(bool)) - { - return new OrType((W)(object)boolean); - } + return new OrType((T)(object)boolean); + } + if (typeof(U) == typeof(bool)) + { + return new OrType((U)(object)boolean); + } + if (typeof(V) == typeof(bool)) + { + return new OrType((V)(object)boolean); + } + if (typeof(W) == typeof(bool)) + { + return new OrType((W)(object)boolean); } throw new InvalidOperationException("Invalid token type for boolean"); } private static OrType ReadStringToken(JsonReader reader, JsonSerializer serializer, Type[] types) { - if (Validators.HasType(types, typeof(string))) + string str = serializer.Deserialize(reader); + if (typeof(T) == typeof(string)) { - string str = serializer.Deserialize(reader); - if (typeof(T) == typeof(string)) - { - return new OrType((T)(object)str); - } - if (typeof(U) == typeof(string)) - { - return new OrType((U)(object)str); - } - if (typeof(V) == typeof(string)) - { - return new OrType((V)(object)str); - } - if (typeof(W) == typeof(string)) - { - return new OrType((W)(object)str); - } + return new OrType((T)(object)str); + } + if (typeof(U) == typeof(string)) + { + return new OrType((U)(object)str); + } + if (typeof(V) == typeof(string)) + { + return new OrType((V)(object)str); + } + if (typeof(W) == typeof(string)) + { + return new OrType((W)(object)str); } throw new InvalidOperationException("Invalid token type for string"); } private static OrType ReadObjectToken(JToken token, JsonSerializer serializer, Type[] types) { + var exceptions = new List(); foreach (Type type in types) { try @@ -557,13 +541,14 @@ private static OrType ReadObjectToken(JToken token, JsonSerializer s } } - catch + catch (Exception ex) { + exceptions.Add(ex); continue; } } - throw new ArgumentException(nameof(token)); + throw new JsonSerializationException("Unable to deserialize object", new AggregateException(exceptions)); } public override void WriteJson(JsonWriter writer, OrType? value, JsonSerializer serializer) diff --git a/generator/plugins/dotnet/custom/ResponseError.cs b/generator/plugins/dotnet/custom/ResponseError.cs new file mode 100644 index 0000000..5151783 --- /dev/null +++ b/generator/plugins/dotnet/custom/ResponseError.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; +using System.Runtime.Serialization; + +[DataContract] +public class ResponseError +{ + [JsonConstructor] + public ResponseError( + int code, + string message, + LSPObject? data = null + ) + { + Code = code; + Message = message; + Data = data; + } + + [DataMember(Name = "code")] + int Code { get; } + + [DataMember(Name = "message")] + string Message { get; } + + [DataMember(Name = "data")] + LSPObject? Data { get; } +} \ No newline at end of file diff --git a/generator/plugins/dotnet/dotnet_classes.py b/generator/plugins/dotnet/dotnet_classes.py index e7113f7..2e598ce 100644 --- a/generator/plugins/dotnet/dotnet_classes.py +++ b/generator/plugins/dotnet/dotnet_classes.py @@ -62,6 +62,8 @@ def lsp_to_base_types(lsp_type: model.BaseType): return "uint" elif lsp_type.name in ["boolean"]: return "bool" + elif lsp_type.name in ["null"]: + return "object" # null should be handled by the caller as an Option<> type raise ValueError(f"Unknown base type: {lsp_type.name}") @@ -96,9 +98,7 @@ def get_type_name( elif type_def.kind == "array": name = f"{get_type_name(type_def.element, types, spec, name_context)}[]" elif type_def.kind == "map": - key_type = get_type_name(type_def.key, types, spec, name_context) - value_type = get_type_name(type_def.value, types, spec, name_context) - name = f"Dictionary<{key_type}, {value_type}>" + name = generate_map_type(type_def, types, spec, name_context) elif type_def.kind == "base": name = lsp_to_base_types(type_def) elif type_def.kind == "literal": @@ -132,6 +132,33 @@ def get_type_name( return name +def generate_map_type( + type_def: model.LSP_TYPE_SPEC, + types: TypeData, + spec: model.LSPModel, + name_context: Optional[str] = None, +) -> str: + key_type = get_type_name(type_def.key, types, spec, name_context) + + if type_def.value.kind == "or": + subset = filter_null_base_type(type_def.value.items) + if len(subset) == 1: + value_type = get_type_name(type_def.value, types, spec, name_context) + else: + value_type = to_upper_camel_case(f"{name_context}Value") + type_alias = model.TypeAlias( + **{ + "name": value_type, + "type": type_def.value, + } + ) + generate_class_from_type_alias(type_alias, spec, types) + + else: + value_type = get_type_name(type_def.value, types, spec, name_context) + return f"Dictionary<{key_type}, {value_type}>" + + 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))]" @@ -143,8 +170,12 @@ def get_converter(type_def: model.LSP_TYPE_SPEC, type_name: str) -> Optional[str elif type_def.kind == "reference" and type_def.name == "DocumentSelector": return "[JsonConverter(typeof(DocumentSelectorConverter))]" elif type_def.kind == "or" and type_name.startswith("OrType<"): - converter = type_name.replace("OrType<", "OrTypeConverter<") - return f"[JsonConverter(typeof({converter}))]" + subset = filter_null_base_type(type_def.items) + if len(subset) == 1: + return get_converter(subset[0], type_name) + elif len(subset) >= 2: + 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: @@ -190,6 +221,8 @@ def generate_property( lines.append( f'public {type_name}{optional} {name} {{ get; set; }} = "{prop_def.type.value}";' ) + elif prop_def.type.kind == "base" and prop_def.type.name == "null": + lines.append(f"public {type_name}{optional} {name} {{ get; set; }} = null;") else: lines.append(f"public {type_name}{optional} {name} {{ get; set; }}") @@ -311,6 +344,7 @@ def generate_class_from_struct( spec: model.LSPModel, types: TypeData, derived: Optional[str] = None, + attributes: Optional[List[str]] = None, ): if types.get_by_name(struct.name) or struct.name.startswith("_"): return @@ -331,7 +365,7 @@ def generate_class_from_struct( lines = namespace_wrapper( NAMESPACE, get_usings(usings), - class_wrapper(struct, inner, derived), + class_wrapper(struct, inner, derived, attributes), ) types.add_type_info(struct, struct.name, lines) @@ -434,6 +468,8 @@ def generate_type_alias_converter( "}", f"public override {type_def.name}? ReadJson(JsonReader reader, Type objectType, {type_def.name}? existingValue, bool hasExistingValue, JsonSerializer serializer)", "{", + "reader = reader ?? throw new ArgumentNullException(nameof(reader));", + "if (reader.TokenType == JsonToken.Null) { return null; }", f"var o = _orType.ReadJson(reader, objectType, existingValue, serializer);", f"if (o is {or_type} orType)", "{", @@ -447,7 +483,7 @@ def generate_type_alias_converter( ] code += [ "}", - "throw new JsonSerializationException($\"Unexpected token type '{{orType.GetType()}}'.\");", + 'throw new JsonSerializationException($"Unexpected token type.");', "}", f"public override void WriteJson(JsonWriter writer, {type_def.name}? value, JsonSerializer serializer)", "{", @@ -780,6 +816,64 @@ def get_message_template( return model.Structure(**class_template) +def get_response_template( + obj: model.Request, spec: model.LSPModel, types: TypeData +) -> model.Structure: + properties = [ + { + "name": "jsonrpc", + "type": {"kind": "stringLiteral", "value": "2.0"}, + "documentation": "The jsonrpc version.", + }, + { + "name": "id", + "type": { + "kind": "or", + "items": [ + {"kind": "base", "name": "string"}, + {"kind": "base", "name": "integer"}, + ], + }, + "documentation": f"The Request id.", + }, + ] + if obj.result: + properties.append( + { + "name": "result", + "type": cattrs.unstructure(obj.result), + "documentation": f"Results for the request.", + "optional": True, + } + ) + else: + properties.append( + { + "name": "result", + "type": {"kind": "base", "name": "null"}, + "documentation": f"Results for the request.", + "optional": True, + } + ) + properties.append( + { + "name": "error", + "type": {"kind": "reference", "name": "ResponseError"}, + "documentation": f"Error while handling the request.", + "optional": True, + } + ) + class_template = { + "name": f"{lsp_method_to_name(obj.method)}Response", + "properties": properties, + "documentation": obj.documentation, + "since": obj.since, + "deprecated": obj.deprecated, + "proposed": obj.proposed, + } + return model.Structure(**class_template) + + def get_registration_options_template( obj: Union[model.Request, model.Notification], spec: model.LSPModel, @@ -819,6 +913,10 @@ def generate_all_classes(spec: model.LSPModel, types: TypeData): generate_request_notification_methods(spec, types) for request in spec.requests: + partial_result_name = None + if request.partialResult: + partial_result_name = get_type_name(request.partialResult, types, spec) + struct = get_message_template(request, is_request=True) generate_class_from_struct( struct, @@ -829,6 +927,22 @@ def generate_all_classes(spec: model.LSPModel, types: TypeData): if request.params else "IRequest" ), + [ + f"[Direction(MessageDirection.{to_upper_camel_case(request.messageDirection)})]", + f'[LSPRequest("{request.method}", typeof({lsp_method_to_name(request.method)}Response), typeof({partial_result_name}))]' + if partial_result_name + else f'[LSPRequest("{request.method}", typeof({lsp_method_to_name(request.method)}Response))]', + ], + ) + response = get_response_template(request, spec, types) + generate_class_from_struct( + response, + spec, + types, + f"IResponse<{get_type_name(request.result, types, spec)}>", + [ + f"[LSPResponse(typeof({lsp_method_to_name(request.method)}Request))]", + ], ) registration_options = get_registration_options_template(request, spec, types) if registration_options: @@ -849,6 +963,9 @@ def generate_all_classes(spec: model.LSPModel, types: TypeData): if notification.params else "INotification" ), + [ + f"[Direction(MessageDirection.{to_upper_camel_case(request.messageDirection)})]", + ], ) registration_options = get_registration_options_template( notification, spec, types diff --git a/generator/plugins/testdata/testdata_generator.py b/generator/plugins/testdata/testdata_generator.py index 3b3d1f0..4d9b850 100644 --- a/generator/plugins/testdata/testdata_generator.py +++ b/generator/plugins/testdata/testdata_generator.py @@ -38,6 +38,15 @@ def request_variants(method: str): yield False, {"id": 1, "method": method} +def response_variants(): + for id_value in [1, LSP_MAX_INT, LSP_MIN_INT, "string-id-1"]: + yield True, {"jsonrpc": "2.0", "id": id_value} + for id_value in [LSP_OVER_MAX_INT, LSP_UNDER_MIN_INT, 1.0, True, None]: + yield False, {"jsonrpc": "2.0", "id": id_value} + yield False, {"jsonrpc": "2.0"} + yield False, {"id": 1} + + def notify_variants(method: str): yield True, {"jsonrpc": "2.0", "method": method} yield False, {"jsonrpc": "2.0", "id": 1, "method": method} @@ -384,6 +393,54 @@ def generate_notifications(notify: model.Notification, spec: model.LSPModel) -> yield (valid1 and valid2, message) +RESPONSE_ERROR = model.Structure( + **{ + "name": "ResponseError", + "properties": [ + { + "name": "code", + "type": {"kind": "base", "name": "integer"}, + }, + { + "name": "message", + "type": {"kind": "base", "name": "string"}, + }, + { + "name": "data", + "type": {"kind": "reference", "name": "LSPObject"}, + "optional": True, + }, + ], + } +) + + +def generate_responses(request: model.Request, spec: model.LSPModel) -> None: + variants = zip( + *extend_all( + [ + list(response_variants()), + list(generate_for_type(request.result, spec, [])), + list( + generate_for_type( + model.ReferenceType("reference", "ResponseError"), spec, [] + ) + ), + ] + ) + ) + + for (valid1, base), (valid2, result), (valid3, error) in variants: + valid = valid1 and valid2 and valid3 + if isinstance(result, Ignore): + yield (valid, base) + else: + message = deepcopy(base) + message.update({"result": result}) + message.update({"error": error}) + yield (valid, message) + + PARTS_RE = re.compile(r"(([a-z0-9])([A-Z]))") @@ -404,6 +461,7 @@ def lsp_method_to_name(method: str) -> str: def generate(spec: model.LSPModel, logger: logging.Logger): + spec.structures.append(RESPONSE_ERROR) testdata = {} for request in spec.requests: counter = 0 @@ -414,7 +472,16 @@ def generate(spec: model.LSPModel, logger: logging.Logger): continue testdata[name] = content counter += 1 - logger.info(f"Generated {counter} variants for: {request.method}") + logger.info(f"Generated {counter} variants for Request: {request.method}") + + for valid, value in generate_responses(request, spec): + content = json.dumps(value, indent=4, ensure_ascii=False) + name = f"{lsp_method_to_name(request.method)}Response-{valid}-{get_hash_from(content)}.json" + if name in testdata: + continue + testdata[name] = content + counter += 1 + logger.info(f"Generated {counter} variants for Response: {request.method}") for notify in spec.notifications: counter = 0 @@ -425,7 +492,7 @@ def generate(spec: model.LSPModel, logger: logging.Logger): continue testdata[name] = content counter += 1 - logger.info(f"Generated {counter} variants for: {notify.method}") + logger.info(f"Generated {counter} variants for Notification: {notify.method}") logger.info(f"Generated {len(testdata)} test variants") return testdata diff --git a/tests/dotnet/lsprotocol_tests/LSPTests.cs b/tests/dotnet/lsprotocol_tests/LSPTests.cs index 4d71450..d25c556 100644 --- a/tests/dotnet/lsprotocol_tests/LSPTests.cs +++ b/tests/dotnet/lsprotocol_tests/LSPTests.cs @@ -50,18 +50,26 @@ private static void RunTest(bool valid, string data, Type type) { if (valid) { - var settings = new JsonSerializerSettings + try { - MissingMemberHandling = MissingMemberHandling.Error - }; - object? deserializedObject = JsonConvert.DeserializeObject(data, type, settings); - string newJson = JsonConvert.SerializeObject(deserializedObject, settings); + 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), $"Failed for : {data}"); + JToken token1 = JToken.Parse(data); + JToken token2 = JToken.Parse(newJson); + RemoveNullProperties(token1); + RemoveNullProperties(token2); + Assert.True(JToken.DeepEquals(token1, token2), $"Failed for : {data}"); + } + catch (Exception e) + { + // Explicitly fail the test + Assert.True(false, $"Should not have thrown an exception for [{type.Name}]: {data} \r\n{e}"); + } } else { @@ -69,7 +77,7 @@ private static void RunTest(bool valid, string data, Type type) { JsonConvert.DeserializeObject(data, type); // Explicitly fail the test - Assert.True(false, $"Should have thrown an exception for : {data}"); + Assert.True(false, $"Should have thrown an exception for [{type.Name}]: {data}"); } catch {