diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 19:55:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 19:55:48 +0000 |
commit | 8be448d3881909fb0ce4b033cad71aa7575de0aa (patch) | |
tree | da33caff06645347a08c3c9c56dd703e4acb5aa3 /generator/plugins/dotnet | |
parent | Initial commit. (diff) | |
download | lsprotocol-8be448d3881909fb0ce4b033cad71aa7575de0aa.tar.xz lsprotocol-8be448d3881909fb0ce4b033cad71aa7575de0aa.zip |
Adding upstream version 2023.0.0.upstream/2023.0.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'generator/plugins/dotnet')
30 files changed, 2853 insertions, 0 deletions
diff --git a/generator/plugins/dotnet/__init__.py b/generator/plugins/dotnet/__init__.py new file mode 100644 index 0000000..8c6065b --- /dev/null +++ b/generator/plugins/dotnet/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .dotnet_utils import generate_from_spec as generate diff --git a/generator/plugins/dotnet/custom/CustomArrayConverter.cs b/generator/plugins/dotnet/custom/CustomArrayConverter.cs new file mode 100644 index 0000000..d34eb62 --- /dev/null +++ b/generator/plugins/dotnet/custom/CustomArrayConverter.cs @@ -0,0 +1,42 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Collections.Immutable; + +public class CustomArrayConverter<T> : JsonConverter<ImmutableArray<T>> +{ + public override ImmutableArray<T> ReadJson(JsonReader reader, Type objectType, ImmutableArray<T> existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return default(ImmutableArray<T>); + } + + JArray array = JArray.Load(reader); + ImmutableArray<T>.Builder builder = ImmutableArray.CreateBuilder<T>(); + + for (int i = 0; i < array.Count; i++) + { + builder.Add((T)array[i].ToObject(typeof(T))!); + } + + return builder.ToImmutable(); + + } + + public override void WriteJson(JsonWriter writer, ImmutableArray<T> value, JsonSerializer serializer) + { + if (value.IsDefault) + { + writer.WriteNull(); + } + else + { + writer.WriteStartArray(); + foreach (var item in value) + { + serializer.Serialize(writer, item); + } + writer.WriteEndArray(); + } + } +}
\ No newline at end of file diff --git a/generator/plugins/dotnet/custom/CustomObjectConverter.cs b/generator/plugins/dotnet/custom/CustomObjectConverter.cs new file mode 100644 index 0000000..e03b588 --- /dev/null +++ b/generator/plugins/dotnet/custom/CustomObjectConverter.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; + + +class CustomObjectConverter<T> : JsonConverter<T> where T : Dictionary<string, object?> +{ + public override T ReadJson(JsonReader reader, Type objectType, T? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return default(T)!; + } + + Dictionary<string, object?>? o = serializer.Deserialize<Dictionary<string, object?>>(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 new file mode 100644 index 0000000..33a04df --- /dev/null +++ b/generator/plugins/dotnet/custom/CustomStringConverter.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; + +public class CustomStringConverter<T> : JsonConverter<T> where T : class +{ + public override T? ReadJson(JsonReader reader, Type objectType, T? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.String && reader.Value is string str) + { + return Activator.CreateInstance(typeof(T), str) as T; + } + else if (reader.TokenType == JsonToken.Null) + { + return null; + } + + throw new JsonSerializationException($"Unexpected token type '{reader.TokenType}' while deserializing '{objectType.Name}'."); + } + + public override void WriteJson(JsonWriter writer, T? value, JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + } + else if (value is Uri u) + { + writer.WriteValue(u.AbsoluteUri); + } + else if (value is T t) + { + writer.WriteValue(t.ToString()); + } + else + { + throw new ArgumentException($"{nameof(value)} must be of type {nameof(T)}."); + } + } +} diff --git a/generator/plugins/dotnet/custom/Direction.cs b/generator/plugins/dotnet/custom/Direction.cs new file mode 100644 index 0000000..757acd3 --- /dev/null +++ b/generator/plugins/dotnet/custom/Direction.cs @@ -0,0 +1,12 @@ +using System; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Enum)] +public class DirectionAttribute : Attribute +{ + public DirectionAttribute(MessageDirection direction) + { + Direction = direction; + } + + public MessageDirection Direction { get; } +}
\ No newline at end of file diff --git a/generator/plugins/dotnet/custom/DocumentSelectorConverter.cs b/generator/plugins/dotnet/custom/DocumentSelectorConverter.cs new file mode 100644 index 0000000..3124416 --- /dev/null +++ b/generator/plugins/dotnet/custom/DocumentSelectorConverter.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; + +public class DocumentSelectorConverter : JsonConverter<DocumentSelector> +{ + public override void WriteJson(JsonWriter writer, DocumentSelector? value, JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + } + else + { + serializer.Serialize(writer, (DocumentFilter[])value); + } + } + + public override DocumentSelector ReadJson(JsonReader reader, Type objectType, DocumentSelector? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null!; + } + var token = JToken.Load(reader); + if (token.Type == JTokenType.Array) + { + var filters = token.ToObject<DocumentFilter[]>(serializer); + return new DocumentSelector(filters ?? Array.Empty<DocumentFilter>()); + } + + throw new JsonSerializationException("Invalid JSON for DocumentSelector"); + } +} diff --git a/generator/plugins/dotnet/custom/IMessage.cs b/generator/plugins/dotnet/custom/IMessage.cs new file mode 100644 index 0000000..175c258 --- /dev/null +++ b/generator/plugins/dotnet/custom/IMessage.cs @@ -0,0 +1,4 @@ +public interface IMessage +{ + string JsonRPC { get; } +}
\ No newline at end of file diff --git a/generator/plugins/dotnet/custom/INotification.cs b/generator/plugins/dotnet/custom/INotification.cs new file mode 100644 index 0000000..b287767 --- /dev/null +++ b/generator/plugins/dotnet/custom/INotification.cs @@ -0,0 +1,6 @@ +public interface INotification<TParams> : IMessage +{ + string Method { get; } + + TParams? Params { get; } +}
\ No newline at end of file diff --git a/generator/plugins/dotnet/custom/IOrType.cs b/generator/plugins/dotnet/custom/IOrType.cs new file mode 100644 index 0000000..217b64a --- /dev/null +++ b/generator/plugins/dotnet/custom/IOrType.cs @@ -0,0 +1,4 @@ +public interface IOrType +{ + public object? Value { get; } +}
\ No newline at end of file diff --git a/generator/plugins/dotnet/custom/IPartialResultParams.cs b/generator/plugins/dotnet/custom/IPartialResultParams.cs new file mode 100644 index 0000000..ef90c3b --- /dev/null +++ b/generator/plugins/dotnet/custom/IPartialResultParams.cs @@ -0,0 +1,15 @@ +using System; + +/// <summary> +/// Interface to describe parameters for requests that support streaming results. +/// +/// See the <see href="https://microsoft.github.io/language-server-protocol/specifications/specification-current/#partialResultParams">Language Server Protocol specification</see> for additional information. +/// </summary> +/// <typeparam name="T">The type to be reported by <see cref="PartialResultToken"/>.</typeparam> +public interface IPartialResultParams +{ + /// <summary> + /// An optional token that a server can use to report partial results (e.g. streaming) to the client. + /// </summary> + public ProgressToken? PartialResultToken { get; set; } +}
\ No newline at end of file diff --git a/generator/plugins/dotnet/custom/IRequest.cs b/generator/plugins/dotnet/custom/IRequest.cs new file mode 100644 index 0000000..55fcbd8 --- /dev/null +++ b/generator/plugins/dotnet/custom/IRequest.cs @@ -0,0 +1,9 @@ +public interface IRequest<TParams> : IMessage +{ + + OrType<string, int> Id { get; } + + string Method { get; } + + TParams Params { get; } +}
\ No newline at end of file diff --git a/generator/plugins/dotnet/custom/IResponse.cs b/generator/plugins/dotnet/custom/IResponse.cs new file mode 100644 index 0000000..01ecf3e --- /dev/null +++ b/generator/plugins/dotnet/custom/IResponse.cs @@ -0,0 +1,9 @@ +public interface IResponse<TResponse> : IMessage +{ + + OrType<string, int> Id { get; } + + TResponse? Result { get; } + + ResponseError? Error { get; } +}
\ No newline at end of file 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<long>(reader)); + + case JsonToken.Float: + return new LSPAny(serializer.Deserialize<float>(reader)); + + case JsonToken.Boolean: + return new LSPAny(serializer.Deserialize<bool>(reader)); + + case JsonToken.String: + return new LSPAny(serializer.Deserialize<string>(reader)); + + case JsonToken.StartArray: + List<object>? l = serializer.Deserialize<List<object>>(reader); + if (l == null) + { + return null; + } + return new LSPAny(new LSPArray(l)); + + case JsonToken.StartObject: + Dictionary<string, object?>? o = serializer.Deserialize<Dictionary<string, object?>>(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/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/MessageDirection.cs b/generator/plugins/dotnet/custom/MessageDirection.cs new file mode 100644 index 0000000..a2792ff --- /dev/null +++ b/generator/plugins/dotnet/custom/MessageDirection.cs @@ -0,0 +1,8 @@ +using System.Runtime.Serialization; + +public enum MessageDirection +{ + [EnumMember(Value = "serverToClient")] ServerToClient, + [EnumMember(Value = "clientToServer")] ClientToServer, + [EnumMember(Value = "both")] Both, +}
\ No newline at end of file diff --git a/generator/plugins/dotnet/custom/OrType.cs b/generator/plugins/dotnet/custom/OrType.cs new file mode 100644 index 0000000..2ddf320 --- /dev/null +++ b/generator/plugins/dotnet/custom/OrType.cs @@ -0,0 +1,138 @@ +using System; + +public record OrType<T, U> : IOrType +{ + public object? Value { get; } + public OrType(T t) + { + Value = t ?? throw new ArgumentNullException(nameof(t)); + } + + public OrType(U u) + { + Value = u ?? throw new ArgumentNullException(nameof(u)); + } + + public static explicit operator U?(OrType<T, U> obj) + { + return obj.Value is U x ? x : default; + } + + public static explicit operator T?(OrType<T, U> obj) + { + return obj.Value is T x ? x : default; + } + + public static explicit operator OrType<T, U>(U obj) => obj is null ? null! : new OrType<T, U>(obj); + public static explicit operator OrType<T, U>(T obj) => obj is null ? null! : new OrType<T, U>(obj); + + public override string ToString() + { + return Value?.ToString()!; + } +} + +public record OrType<T, U, V> : IOrType +{ + public object? Value { get; } + + public OrType(T t) + { + Value = t ?? throw new ArgumentNullException(nameof(t)); + } + + public OrType(U u) + { + Value = u ?? throw new ArgumentNullException(nameof(u)); + } + + public OrType(V v) + { + Value = v ?? throw new ArgumentNullException(nameof(v)); + } + + public static explicit operator U?(OrType<T, U, V> obj) + { + return obj.Value is U x ? x : default; + } + + public static explicit operator T?(OrType<T, U, V> obj) + { + return obj.Value is T x ? x : default; + } + + public static explicit operator V?(OrType<T, U, V> obj) + { + return obj.Value is V x ? x : default; + } + + public static explicit operator OrType<T, U, V>(U obj) => obj is null ? null! : new OrType<T, U, V>(obj); + + public static explicit operator OrType<T, U, V>(T obj) => obj is null ? null! : new OrType<T, U, V>(obj); + + public static explicit operator OrType<T, U, V>(V obj) => obj is null ? null! : new OrType<T, U, V>(obj); + + public override string ToString() + { + return Value?.ToString()!; + } +} + + +public record OrType<T, U, V, W> : IOrType +{ + public object? Value { get; } + + public OrType(T t) + { + Value = t ?? throw new ArgumentNullException(nameof(t)); + } + + public OrType(U u) + { + Value = u ?? throw new ArgumentNullException(nameof(u)); + } + + public OrType(V v) + { + Value = v ?? throw new ArgumentNullException(nameof(v)); + } + + public OrType(W w) + { + Value = w ?? throw new ArgumentNullException(nameof(w)); + } + + public static explicit operator U?(OrType<T, U, V, W> obj) + { + return obj.Value is U x ? x : default; + } + + public static explicit operator T?(OrType<T, U, V, W> obj) + { + return obj.Value is T x ? x : default; + } + + public static explicit operator V?(OrType<T, U, V, W> obj) + { + return obj.Value is V x ? x : default; + } + + public static explicit operator W?(OrType<T, U, V, W> obj) + { + return obj.Value is W x ? x : default; + } + + public static explicit operator OrType<T, U, V, W>(U obj) => obj is null ? null! : new OrType<T, U, V, W>(obj); + + public static explicit operator OrType<T, U, V, W>(T obj) => obj is null ? null! : new OrType<T, U, V, W>(obj); + + public static explicit operator OrType<T, U, V, W>(V obj) => obj is null ? null! : new OrType<T, U, V, W>(obj); + + public static explicit operator OrType<T, U, V, W>(W obj) => obj is null ? null! : new OrType<T, U, V, W>(obj); + + public override string ToString() + { + return Value?.ToString()!; + } +} diff --git a/generator/plugins/dotnet/custom/OrTypeArrayConverter.cs b/generator/plugins/dotnet/custom/OrTypeArrayConverter.cs new file mode 100644 index 0000000..f2dbf23 --- /dev/null +++ b/generator/plugins/dotnet/custom/OrTypeArrayConverter.cs @@ -0,0 +1,145 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Immutable; + +public class OrTypeArrayConverter<T, U> : JsonConverter<ImmutableArray<OrType<T, U>>> +{ + private OrTypeConverter<T, U> _converter; + + public OrTypeArrayConverter() + { + _converter = new OrTypeConverter<T, U>(); + } + + public override ImmutableArray<OrType<T, U>> ReadJson(JsonReader reader, Type objectType, ImmutableArray<OrType<T, U>> existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return default(ImmutableArray<OrType<T, U>>); + } + + JArray array = JArray.Load(reader); + ImmutableArray<OrType<T, U>>.Builder builder = ImmutableArray.CreateBuilder<OrType<T, U>>(); + + for (int i = 0; i < array.Count; i++) + { + builder.Add((OrType<T, U>)_converter.ReadJson(array[i].CreateReader(), typeof(OrType<T, U>), null, serializer)!); + } + + return builder.ToImmutable(); + } + + public override void WriteJson(JsonWriter writer, ImmutableArray<OrType<T, U>> value, JsonSerializer serializer) + { + if (value.IsDefault) + { + writer.WriteNull(); + } + else + { + writer.WriteStartArray(); + + foreach (var item in value) + { + _converter.WriteJson(writer, item, serializer); + } + + writer.WriteEndArray(); + } + } +} +public class OrTypeArrayConverter<T, U, V> : JsonConverter<ImmutableArray<OrType<T, U, V>>> +{ + private OrTypeConverter<T, U, V> _converter; + + public OrTypeArrayConverter() + { + _converter = new OrTypeConverter<T, U, V>(); + } + + public override ImmutableArray<OrType<T, U, V>> ReadJson(JsonReader reader, Type objectType, ImmutableArray<OrType<T, U, V>> existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return default(ImmutableArray<OrType<T, U, V>>); + } + + JArray array = JArray.Load(reader); + ImmutableArray<OrType<T, U, V>>.Builder builder = ImmutableArray.CreateBuilder<OrType<T, U, V>>(); + + for (int i = 0; i < array.Count; i++) + { + builder.Add((OrType<T, U, V>)_converter.ReadJson(array[i].CreateReader(), typeof(OrType<T, U, V>), null, serializer)!); + } + + return builder.ToImmutable(); + } + + public override void WriteJson(JsonWriter writer, ImmutableArray<OrType<T, U, V>> value, JsonSerializer serializer) + { + if (value.IsDefault) + { + writer.WriteNull(); + } + else + { + writer.WriteStartArray(); + + foreach (var item in value) + { + _converter.WriteJson(writer, item, serializer); + } + + writer.WriteEndArray(); + } + } +} + + +public class OrTypeArrayConverter<T, U, V, W> : JsonConverter<ImmutableArray<OrType<T, U, V, W>>> +{ + private OrTypeConverter<T, U, V, W> _converter; + + public OrTypeArrayConverter() + { + _converter = new OrTypeConverter<T, U, V, W>(); + } + + public override ImmutableArray<OrType<T, U, V, W>> ReadJson(JsonReader reader, Type objectType, ImmutableArray<OrType<T, U, V, W>> existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return default(ImmutableArray<OrType<T, U, V, W>>); + } + + JArray array = JArray.Load(reader); + ImmutableArray<OrType<T, U, V, W>>.Builder builder = ImmutableArray.CreateBuilder<OrType<T, U, V, W>>(); + + for (int i = 0; i < array.Count; i++) + { + builder.Add((OrType<T, U, V, W>)_converter.ReadJson(array[i].CreateReader(), typeof(OrType<T, U, V, W>), null, serializer)!); + } + + return builder.ToImmutable(); + } + + public override void WriteJson(JsonWriter writer, ImmutableArray<OrType<T, U, V, W>> value, JsonSerializer serializer) + { + if (value.IsDefault) + { + 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 new file mode 100644 index 0000000..f2aadbf --- /dev/null +++ b/generator/plugins/dotnet/custom/OrTypeConverter.cs @@ -0,0 +1,595 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; + +internal class OrTypeConverterHelpers +{ + public static Type[] SortTypesByHeuristic(Type[] types, JToken jToken) + { + var typePropertyScores = new Dictionary<Type, int>(); + + string[] jTokenPropertyNames = jToken.Children<JProperty>().Select(p => p.Name.ToUpper()).ToArray(); + + foreach (Type type in types) + { + string[] typePropertyNames = type.GetProperties().Select(p => p.Name.ToUpper()).ToArray(); + + int score = jTokenPropertyNames.Count(propertyName => typePropertyNames.Contains(propertyName)); + typePropertyScores[type] = score; + } + + return types.OrderByDescending(type => typePropertyScores[type]).ToArray(); + } +} + +public class OrTypeConverter<T, U> : JsonConverter<OrType<T, U>> +{ + public override OrType<T, U>? ReadJson(JsonReader reader, Type objectType, OrType<T, U>? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + reader = reader ?? throw new ArgumentNullException(nameof(reader)); + + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + Type[] types = new Type[] { typeof(T), typeof(U) }; + + if (reader.TokenType == JsonToken.Integer && (Validators.HasType(types, typeof(uint)) || Validators.HasType(types, typeof(int)))) + { + 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); + } + + var token = JToken.Load(reader); + return OrTypeConverter<T, U>.ReadObjectToken(token, serializer, OrTypeConverterHelpers.SortTypesByHeuristic(types, token)); + } + + private static OrType<T, U> ReadIntegerToken(JsonReader reader, JsonSerializer serializer, Type[] types) + { + long integer = serializer.Deserialize<long>(reader); + if (Validators.InUIntegerRange(integer) && Validators.HasType(types, typeof(uint))) + { + if (typeof(T) == typeof(uint)) + { + return new OrType<T, U>((T)(object)(uint)integer); + } + if (typeof(U) == typeof(uint)) + { + return new OrType<T, U>((U)(object)(uint)integer); + } + } + if (Validators.InIntegerRange(integer) && Validators.HasType(types, typeof(int))) + { + if (typeof(T) == typeof(int)) + { + return new OrType<T, U>((T)(object)(int)integer); + } + if (typeof(U) == typeof(int)) + { + return new OrType<T, U>((U)(object)(int)integer); + } + } + throw new ArgumentOutOfRangeException($"Integer out-of-range of LSP Signed Integer[{int.MinValue}:{int.MaxValue}] and out-of-range of LSP Unsigned Integer [{uint.MinValue}:{uint.MaxValue}] => {integer}"); + } + + private static OrType<T, U> ReadFloatToken(JsonReader reader, JsonSerializer serializer, Type[] types) + { + float real = serializer.Deserialize<float>(reader); + if (typeof(T) == typeof(float)) + { + return new OrType<T, U>((T)(object)real); + } + if (typeof(U) == typeof(float)) + { + return new OrType<T, U>((U)(object)real); + } + throw new InvalidOperationException("Invalid token type for float"); + } + + private static OrType<T, U> ReadBooleanToken(JsonReader reader, JsonSerializer serializer, Type[] types) + { + bool boolean = serializer.Deserialize<bool>(reader); + if (typeof(T) == typeof(bool)) + { + return new OrType<T, U>((T)(object)boolean); + } + if (typeof(U) == typeof(bool)) + { + return new OrType<T, U>((U)(object)boolean); + } + throw new InvalidOperationException("Invalid token type for boolean"); + } + + private static OrType<T, U> ReadStringToken(JsonReader reader, JsonSerializer serializer, Type[] types) + { + string str = serializer.Deserialize<string>(reader)!; + if (typeof(T) == typeof(string)) + { + return new OrType<T, U>((T)(object)str); + } + if (typeof(U) == typeof(string)) + { + return new OrType<T, U>((U)(object)str); + } + throw new InvalidOperationException("Invalid token type for string"); + } + + private static OrType<T, U> ReadObjectToken(JToken token, JsonSerializer serializer, Type[] types) + { + var exceptions = new List<Exception>(); + foreach (Type type in types) + { + try + { + object? value = null; + if (token.Type == JTokenType.Array && type == typeof((uint, uint))) + { + uint[]? o = token.ToObject<uint[]>(serializer); + if (o != null) + { + value = (o[0], o[1]); + } + } + else + { + value = token.ToObject(type, serializer); + } + + if (value != null) + { + if (value is T t) + { + return new OrType<T, U>(t); + } + if (value is U u) + { + return new OrType<T, U>(u); + } + } + } + catch (Exception ex) + { + exceptions.Add(ex); + continue; + } + } + + throw new JsonSerializationException("Unable to deserialize object", new AggregateException(exceptions)); + } + + + + public override void WriteJson(JsonWriter writer, OrType<T, U>? value, JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + } + else if (value?.Value?.GetType() == typeof((uint, uint))) + { + ValueTuple<uint, uint> o = (ValueTuple<uint, uint>)(value.Value); + serializer.Serialize(writer, new uint[] { o.Item1, o.Item2 }); + } + else + { + serializer.Serialize(writer, value?.Value); + } + } +} + +public class OrTypeConverter<T, U, V> : JsonConverter<OrType<T, U, V>> +{ + public override OrType<T, U, V>? ReadJson(JsonReader reader, Type objectType, OrType<T, U, V>? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + reader = reader ?? throw new ArgumentNullException(nameof(reader)); + + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + Type[] types = new Type[] { typeof(T), typeof(U), typeof(V) }; + + if (reader.TokenType == JsonToken.Integer && (Validators.HasType(types, typeof(uint)) || Validators.HasType(types, typeof(int)))) + { + 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); + } + + var token = JToken.Load(reader); + return OrTypeConverter<T, U, V>.ReadObjectToken(token, serializer, OrTypeConverterHelpers.SortTypesByHeuristic(types, token)); + } + + private static OrType<T, U, V> ReadIntegerToken(JsonReader reader, JsonSerializer serializer, Type[] types) + { + long integer = serializer.Deserialize<long>(reader); + if (Validators.InUIntegerRange(integer) && Validators.HasType(types, typeof(uint))) + { + if (typeof(T) == typeof(uint)) + { + return new OrType<T, U, V>((T)(object)(uint)integer); + } + if (typeof(U) == typeof(uint)) + { + return new OrType<T, U, V>((U)(object)(uint)integer); + } + if (typeof(V) == typeof(uint)) + { + return new OrType<T, U, V>((V)(object)(uint)integer); + } + } + if (Validators.InIntegerRange(integer) && Validators.HasType(types, typeof(int))) + { + if (typeof(T) == typeof(int)) + { + return new OrType<T, U, V>((T)(object)(int)integer); + } + if (typeof(U) == typeof(int)) + { + return new OrType<T, U, V>((U)(object)(int)integer); + } + if (typeof(V) == typeof(int)) + { + return new OrType<T, U, V>((V)(object)(int)integer); + } + } + throw new ArgumentOutOfRangeException($"Integer out-of-range of LSP Signed Integer[{int.MinValue}:{int.MaxValue}] and out-of-range of LSP Unsigned Integer [{uint.MinValue}:{uint.MaxValue}] => {integer}"); + } + + private static OrType<T, U, V> ReadFloatToken(JsonReader reader, JsonSerializer serializer, Type[] types) + { + float real = serializer.Deserialize<float>(reader); + if (typeof(T) == typeof(float)) + { + return new OrType<T, U, V>((T)(object)real); + } + if (typeof(U) == typeof(float)) + { + return new OrType<T, U, V>((U)(object)real); + } + if (typeof(V) == typeof(float)) + { + return new OrType<T, U, V>((V)(object)real); + } + throw new InvalidOperationException("Invalid token type for float"); + } + + private static OrType<T, U, V> ReadBooleanToken(JsonReader reader, JsonSerializer serializer, Type[] types) + { + bool boolean = serializer.Deserialize<bool>(reader); + if (typeof(T) == typeof(bool)) + { + return new OrType<T, U, V>((T)(object)boolean); + } + if (typeof(U) == typeof(bool)) + { + return new OrType<T, U, V>((U)(object)boolean); + } + if (typeof(V) == typeof(bool)) + { + return new OrType<T, U, V>((V)(object)boolean); + } + throw new InvalidOperationException("Invalid token type for boolean"); + } + + private static OrType<T, U, V> ReadStringToken(JsonReader reader, JsonSerializer serializer, Type[] types) + { + string str = serializer.Deserialize<string>(reader)!; + if (typeof(T) == typeof(string)) + { + return new OrType<T, U, V>((T)(object)str); + } + if (typeof(U) == typeof(string)) + { + return new OrType<T, U, V>((U)(object)str); + } + if (typeof(V) == typeof(string)) + { + return new OrType<T, U, V>((V)(object)str); + } + throw new InvalidOperationException("Invalid token type for string"); + } + + private static OrType<T, U, V> ReadObjectToken(JToken token, JsonSerializer serializer, Type[] types) + { + var exceptions = new List<Exception>(); + foreach (Type type in types) + { + try + { + object? value = null; + if (token.Type == JTokenType.Array && type == typeof((uint, uint))) + { + uint[]? o = token.ToObject<uint[]>(serializer); + if (o != null) + { + value = (o[0], o[1]); + } + } + else + { + value = token.ToObject(type, serializer); + } + + if (value != null) + { + if (value is T t) + { + return new OrType<T, U, V>(t); + } + if (value is U u) + { + return new OrType<T, U, V>(u); + } + if (value is V v) + { + return new OrType<T, U, V>(v); + } + } + } + catch (Exception ex) + { + exceptions.Add(ex); + continue; + } + } + + throw new JsonSerializationException("Unable to deserialize object", new AggregateException(exceptions)); + } + + public override void WriteJson(JsonWriter writer, OrType<T, U, V>? value, JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + } + else if (value?.Value?.GetType() == typeof((uint, uint))) + { + ValueTuple<uint, uint> o = (ValueTuple<uint, uint>)(value.Value); + serializer.Serialize(writer, new uint[] { o.Item1, o.Item2 }); + } + else + { + serializer.Serialize(writer, value?.Value); + } + } +} + +public class OrTypeConverter<T, U, V, W> : JsonConverter<OrType<T, U, V, W>> +{ + public override OrType<T, U, V, W>? ReadJson(JsonReader reader, Type objectType, OrType<T, U, V, W>? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + reader = reader ?? throw new ArgumentNullException(nameof(reader)); + + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + Type[] types = new Type[] { typeof(T), typeof(U), typeof(V), typeof(W) }; + + if (reader.TokenType == JsonToken.Integer && (Validators.HasType(types, typeof(uint)) || Validators.HasType(types, typeof(int)))) + { + 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); + } + + var token = JToken.Load(reader); + return OrTypeConverter<T, U, V, W>.ReadObjectToken(token, serializer, OrTypeConverterHelpers.SortTypesByHeuristic(types, token)); + } + + private static OrType<T, U, V, W> ReadIntegerToken(JsonReader reader, JsonSerializer serializer, Type[] types) + { + long integer = serializer.Deserialize<long>(reader); + if (Validators.InUIntegerRange(integer) && Validators.HasType(types, typeof(uint))) + { + if (typeof(T) == typeof(uint)) + { + return new OrType<T, U, V, W>((T)(object)(uint)integer); + } + if (typeof(U) == typeof(uint)) + { + return new OrType<T, U, V, W>((U)(object)(uint)integer); + } + if (typeof(V) == typeof(uint)) + { + return new OrType<T, U, V, W>((V)(object)(uint)integer); + } + if (typeof(W) == typeof(uint)) + { + return new OrType<T, U, V, W>((W)(object)(uint)integer); + } + } + if (Validators.InIntegerRange(integer) && Validators.HasType(types, typeof(int))) + { + if (typeof(T) == typeof(int)) + { + return new OrType<T, U, V, W>((T)(object)(int)integer); + } + if (typeof(U) == typeof(int)) + { + return new OrType<T, U, V, W>((U)(object)(int)integer); + } + if (typeof(V) == typeof(int)) + { + return new OrType<T, U, V, W>((V)(object)(int)integer); + } + if (typeof(W) == typeof(int)) + { + return new OrType<T, U, V, W>((W)(object)(int)integer); + } + } + throw new ArgumentOutOfRangeException($"Integer out-of-range of LSP Signed Integer[{int.MinValue}:{int.MaxValue}] and out-of-range of LSP Unsigned Integer [{uint.MinValue}:{uint.MaxValue}] => {integer}"); + } + + private static OrType<T, U, V, W> ReadFloatToken(JsonReader reader, JsonSerializer serializer, Type[] types) + { + float real = serializer.Deserialize<float>(reader); + if (typeof(T) == typeof(float)) + { + return new OrType<T, U, V, W>((T)(object)real); + } + if (typeof(U) == typeof(float)) + { + return new OrType<T, U, V, W>((U)(object)real); + } + if (typeof(V) == typeof(float)) + { + return new OrType<T, U, V, W>((V)(object)real); + } + if (typeof(W) == typeof(float)) + { + return new OrType<T, U, V, W>((W)(object)real); + } + throw new InvalidOperationException("Invalid token type for float"); + } + + private static OrType<T, U, V, W> ReadBooleanToken(JsonReader reader, JsonSerializer serializer, Type[] types) + { + bool boolean = serializer.Deserialize<bool>(reader); + if (typeof(T) == typeof(bool)) + { + return new OrType<T, U, V, W>((T)(object)boolean); + } + if (typeof(U) == typeof(bool)) + { + return new OrType<T, U, V, W>((U)(object)boolean); + } + if (typeof(V) == typeof(bool)) + { + return new OrType<T, U, V, W>((V)(object)boolean); + } + if (typeof(W) == typeof(bool)) + { + return new OrType<T, U, V, W>((W)(object)boolean); + } + throw new InvalidOperationException("Invalid token type for boolean"); + } + + private static OrType<T, U, V, W> ReadStringToken(JsonReader reader, JsonSerializer serializer, Type[] types) + { + string str = serializer.Deserialize<string>(reader)!; + if (typeof(T) == typeof(string)) + { + return new OrType<T, U, V, W>((T)(object)str); + } + if (typeof(U) == typeof(string)) + { + return new OrType<T, U, V, W>((U)(object)str); + } + if (typeof(V) == typeof(string)) + { + return new OrType<T, U, V, W>((V)(object)str); + } + if (typeof(W) == typeof(string)) + { + return new OrType<T, U, V, W>((W)(object)str); + } + throw new InvalidOperationException("Invalid token type for string"); + } + + private static OrType<T, U, V, W> ReadObjectToken(JToken token, JsonSerializer serializer, Type[] types) + { + var exceptions = new List<Exception>(); + foreach (Type type in types) + { + try + { + object? value = null; + if (token.Type == JTokenType.Array && type == typeof((uint, uint))) + { + uint[]? o = token.ToObject<uint[]>(serializer); + if (o != null) + { + value = (o[0], o[1]); + } + } + else + { + value = token.ToObject(type, serializer); + } + + if (value != null) + { + if (value is T t) + { + return new OrType<T, U, V, W>(t); + } + if (value is U u) + { + return new OrType<T, U, V, W>(u); + } + if (value is V v) + { + return new OrType<T, U, V, W>(v); + } + if (value is W w) + { + return new OrType<T, U, V, W>(w); + } + } + + } + catch (Exception ex) + { + exceptions.Add(ex); + continue; + } + } + + throw new JsonSerializationException("Unable to deserialize object", new AggregateException(exceptions)); + } + + public override void WriteJson(JsonWriter writer, OrType<T, U, V, W>? value, JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + } + else if (value?.Value?.GetType() == typeof((uint, uint))) + { + ValueTuple<uint, uint> o = (ValueTuple<uint, uint>)(value.Value); + serializer.Serialize(writer, new uint[] { o.Item1, o.Item2 }); + } + else + { + serializer.Serialize(writer, value?.Value); + } + } +}
\ No newline at end of file diff --git a/generator/plugins/dotnet/custom/Proposed.cs b/generator/plugins/dotnet/custom/Proposed.cs new file mode 100644 index 0000000..eb0ed75 --- /dev/null +++ b/generator/plugins/dotnet/custom/Proposed.cs @@ -0,0 +1,17 @@ +using System; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Enum)] +public class ProposedAttribute : Attribute +{ + public ProposedAttribute() + { + Version = null; + } + + public ProposedAttribute(string version) + { + Version = version; + } + + public string? Version { get; } +}
\ No newline at end of file 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/custom/Since.cs b/generator/plugins/dotnet/custom/Since.cs new file mode 100644 index 0000000..b13da74 --- /dev/null +++ b/generator/plugins/dotnet/custom/Since.cs @@ -0,0 +1,17 @@ +using System; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Enum | AttributeTargets.Interface)] +public class SinceAttribute : Attribute +{ + public SinceAttribute() + { + Version = null; + } + + public SinceAttribute(string version) + { + Version = version; + } + + public string? Version { get; } +}
\ No newline at end of file diff --git a/generator/plugins/dotnet/custom/Validators.cs b/generator/plugins/dotnet/custom/Validators.cs new file mode 100644 index 0000000..9edc18f --- /dev/null +++ b/generator/plugins/dotnet/custom/Validators.cs @@ -0,0 +1,20 @@ + +using System; + +public static class Validators +{ + public static bool HasType(Type[] types, Type type) + { + return types.Contains(type); + } + + public static bool InIntegerRange(long value) + { + return value >= int.MinValue && value <= int.MaxValue; + } + + public static bool InUIntegerRange(long value) + { + return value >= uint.MinValue && value <= uint.MaxValue; + } +}
\ No newline at end of file diff --git a/generator/plugins/dotnet/dotnet_classes.py b/generator/plugins/dotnet/dotnet_classes.py new file mode 100644 index 0000000..0a705cd --- /dev/null +++ b/generator/plugins/dotnet/dotnet_classes.py @@ -0,0 +1,1024 @@ +# 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 + +from generator import model + +from .dotnet_commons import TypeData +from .dotnet_constants import NAMESPACE +from .dotnet_helpers import ( + class_wrapper, + generate_extras, + get_doc, + get_special_case_class_name, + get_special_case_property_name, + get_usings, + indent_lines, + lsp_method_to_name, + namespace_wrapper, + to_camel_case, + to_upper_camel_case, +) + +ORTYPE_CONVERTER_RE = re.compile(r"OrType<(?P<parts>.*)>") +IMMUTABLE_ARRAY_CONVERTER_RE = re.compile(r"ImmutableArray<(?P<elements>.*)>") + + +def _get_enum(name: str, spec: model.LSPModel) -> Optional[model.Enum]: + for enum in spec.enumerations: + if enum.name == name: + return enum + return None + + +def _get_struct(name: str, spec: model.LSPModel) -> Optional[model.Structure]: + for struct in spec.structures: + if struct.name == name: + return struct + return None + + +def _is_str_enum(enum_def: model.Enum) -> bool: + return all(isinstance(item.value, str) for item in enum_def.values) + + +def _is_int_enum(enum_def: model.Enum) -> bool: + return all(isinstance(item.value, int) for item in enum_def.values) + + +def lsp_to_base_types(lsp_type: model.BaseType): + if lsp_type.name in ["string", "RegExp"]: + return "string" + elif lsp_type.name in ["DocumentUri", "URI"]: + return "Uri" + elif lsp_type.name in ["decimal"]: + return "float" + elif lsp_type.name in ["integer"]: + return "int" + elif lsp_type.name in ["uinteger"]: + 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}") + + +def get_types_for_usings(code: List[str]) -> List[str]: + immutable = [] + for line in code: + if "ImmutableArray<" in line: + immutable.append("ImmutableArray") + if "ImmutableDictionary<" in line: + immutable.append("ImmutableDictionary") + return list(set(immutable)) + + +def has_null_base_type(items: List[model.LSP_TYPE_SPEC]) -> bool: + return any(item.kind == "base" and item.name == "null" for item in items) + + +def filter_null_base_type( + items: List[model.LSP_TYPE_SPEC], +) -> List[model.LSP_TYPE_SPEC]: + return [item for item in items if not (item.kind == "base" and item.name == "null")] + + +def get_type_name( + type_def: model.LSP_TYPE_SPEC, + types: TypeData, + spec: model.LSPModel, + name_context: Optional[str] = None, +) -> str: + name = None + if type_def.kind == "reference": + enum_def = _get_enum(type_def.name, spec) + if enum_def and enum_def.supportsCustomValues: + if _is_str_enum(enum_def): + name = f"string" + elif _is_int_enum(enum_def): + name = f"int" + else: + name = get_special_case_class_name(type_def.name) + elif type_def.kind == "array": + name = f"ImmutableArray<{get_type_name(type_def.element, types, spec, name_context)}>" + elif type_def.kind == "map": + 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": + name = generate_literal_type(type_def, types, spec, name_context) + elif type_def.kind == "stringLiteral": + name = "string" + elif type_def.kind == "tuple": + subset = filter_null_base_type(type_def.items) + subset_types = [ + get_type_name(item, types, spec, name_context) for item in subset + ] + name = f"({', '.join(subset_types)})" + elif type_def.kind == "or": + subset = filter_null_base_type(type_def.items) + if len(subset) == 1: + name = get_type_name(subset[0], types, spec, name_context) + elif len(subset) >= 2: + if are_variant_literals(subset): + name = generate_class_from_variant_literals( + subset, spec, types, name_context + ) + else: + subset_types = [ + get_type_name(item, types, spec, name_context) for item in subset + ] + name = f"OrType<{', '.join(subset_types)}>" + else: + raise ValueError(f"Unknown type kind: {type_def.kind}") + else: + raise ValueError(f"Unknown type kind: {type_def.kind}") + 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"ImmutableDictionary<{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<Uri>))]" + elif type_def.kind == "reference" and type_def.name in [ + "Pattern", + "ChangeAnnotationIdentifier", + ]: + 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) == 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 = ORTYPE_CONVERTER_RE.match(type_name).groupdict() + if "parts" in matches: + converter = f"OrTypeArrayConverter<{matches['parts']}>" + return f"[JsonConverter(typeof({converter}))]" + elif type_def.kind == "array": + matches = IMMUTABLE_ARRAY_CONVERTER_RE.match(type_name).groupdict() + elements = matches["elements"] + if elements.startswith("OrType<"): + matches = ORTYPE_CONVERTER_RE.match(elements).groupdict() + converter = f"OrTypeArrayConverter<{matches['parts']}>" + return f"[JsonConverter(typeof({converter}))]" + else: + converter = f"CustomArrayConverter<{elements}>" + return f"[JsonConverter(typeof({converter}))]" + return None + + +def generate_property( + prop_def: model.Property, + spec: model.LSPModel, + types: TypeData, + usings: List[str], + class_name: str = "", +) -> Tuple[List[str], str]: + if prop_def.name == "jsonrpc": + name = "JsonRPC" + else: + name = to_upper_camel_case(prop_def.name) + type_name = get_type_name( + 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 + ) + optional = ( + "?" + if (prop_def.optional or special_optional) + and not ( + type_name.startswith("ImmutableArray<") + or type_name.startswith("ImmutableDictionary<") + ) + else "" + ) + lines = ( + get_doc(prop_def.documentation) + + generate_extras(prop_def) + + ([converter] if converter else []) + + ( + ["[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]"] + if optional and not special_optional + else [] + ) + + [ + f'[DataMember(Name = "{prop_def.name}")]', + ] + ) + + if prop_def.type.kind == "stringLiteral": + lines.append( + f'public {type_name}{optional} {name} {{ get; init; }} = "{prop_def.type.value}";' + ) + else: + lines.append(f"public {type_name}{optional} {name} {{ get; init; }}") + + usings.append("DataMember") + if converter: + usings.append("JsonConverter") + if optional and not special_optional: + usings.append("JsonProperty") + + return lines, type_name + + +def generate_name(name_context: str, types: TypeData) -> str: + # If name context has a '_' it is likely a property. + # Try name generation using just the property name + parts = [to_upper_camel_case(p) for p in name_context.split("_") if len(p) > 3] + + # Try the last part of the name context + name = parts[-1] + if not types.get_by_name(name) and "info" in name_context.lower(): + return name + + # Combine all parts and try again + name = "".join(parts) + if not types.get_by_name(name): + return name + + raise ValueError(f"Unable to generate name for {name_context}") + + +def generate_literal_type( + literal: model.LiteralType, + types: TypeData, + spec: model.LSPModel, + name_context: Optional[str] = None, +) -> str: + if len(literal.value.properties) == 0: + return "LSPObject" + + if types.get_by_name(literal.name) and not _get_struct(literal.name, spec): + return literal.name + + if name_context is None: + raise ValueError("name_context must be provided for literal types") + + if name_context.startswith("I") and name_context[1].isupper(): + # This is a interface name ISomething => Something + name_context = name_context[1:] + + if "_" not in name_context: + name_context = f"{name_context}_{get_context_from_literal(literal)}" + + literal.name = generate_name(name_context, types) + + usings = ["DataContract"] + inner = [] + for prop in literal.value.properties: + prop_code, _ = generate_property(prop, spec, types, usings, literal.name) + inner += prop_code + + lines = namespace_wrapper( + NAMESPACE, + get_usings(usings + get_types_for_usings(inner)), + class_wrapper(literal, inner), + ) + types.add_type_info(literal, literal.name, lines) + return literal.name + + +def generate_constructor( + struct: model.Structure, + types: TypeData, + properties: List[Tuple[model.Property, str]], +) -> List[str]: + class_name = get_special_case_class_name(struct.name) + constructor = [ + "[JsonConstructor]", + f"public {class_name}(", + ] + + arguments = [] + optional_args = [] + assignments = [] + ctor_data = [] + for prop, prop_type in properties: + name = get_special_case_property_name(to_camel_case(prop.name)) + special_optional = prop.type.kind == "or" and has_null_base_type( + prop.type.items + ) + if prop.optional or special_optional: + if prop_type.startswith("ImmutableArray<") or prop_type.startswith( + "ImmutableDictionary<" + ): + optional_args += [f"{prop_type} {name} = default!"] + else: + optional_args += [f"{prop_type}? {name} = null"] + ctor_data += [(prop_type, name, True)] + elif prop.name == "jsonrpc": + optional_args += [f'{prop_type} {name} = "2.0"'] + ctor_data += [(prop_type, name, True)] + else: + arguments += [f"{prop_type} {name}"] + ctor_data += [(prop_type, name, False)] + + if prop.name == "jsonrpc": + assignments += [f"JsonRPC = {name};"] + else: + assignments += [f"{to_upper_camel_case(prop.name)} = {name};"] + + # combine args with a '\n' to get comma with indent + all_args = (",\n".join(indent_lines(arguments + optional_args))).splitlines() + types.add_ctor(struct.name, ctor_data) + + # re-split args to get the right coma placement and indent + constructor += all_args + constructor += [")", "{"] + constructor += indent_lines(assignments) + constructor += ["}"] + return constructor + + +def generate_class_from_struct( + struct: model.Structure, + 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 + + if attributes is None: + attributes = [] + + inner = [] + usings = ["DataContract", "JsonConstructor"] + + properties = get_all_properties(struct, spec) + prop_types = [] + for prop in properties: + prop_code, prop_type = generate_property(prop, spec, types, usings, struct.name) + inner += prop_code + prop_types += [prop_type] + + ctor = generate_constructor(struct, types, zip(properties, prop_types)) + inner = ctor + inner + + lines = namespace_wrapper( + NAMESPACE, + get_usings(usings + get_types_for_usings(inner + attributes)), + class_wrapper(struct, inner, derived, attributes), + ) + types.add_type_info(struct, struct.name, lines) + + +def get_context_from_literal(literal: model.LiteralType) -> str: + if len(literal.value.properties) == 0: + return "LSPObject" + + skipped = 0 + skip = [ + "range", + "rangeLength", + "position", + "position", + "location", + "locationLink", + "text", + ] + for prop in literal.value.properties: + if prop.name in skip: + skipped += 1 + continue + return prop.name + + if skipped == len(literal.value.properties): + # pick property with longest name + names = sorted([p.name for p in literal.value.properties]) + return sorted(names, key=lambda n: len(n))[-1] + + return "" + + +def generate_type_alias_constructor( + type_def: model.TypeAlias, spec: model.LSPModel, types: TypeData +) -> List[str]: + constructor = [] + + if type_def.type.kind == "or": + subset = filter_null_base_type(type_def.type.items) + if len(subset) == 1: + raise ValueError("Unable to generate constructor for single item union") + elif len(subset) >= 2: + type_name = to_upper_camel_case(type_def.name) + for t in subset: + sub_type = get_type_name(t, types, spec, type_def.name) + arg = get_special_case_property_name(to_camel_case(sub_type)) + matches = re.match(r"ImmutableArray<(?P<arg>\w+)>", arg) + if matches: + arg = f"{matches['arg']}s" + + constructor += [ + f"public {type_name}({sub_type} {arg}): base({arg}) {{}}", + ] + else: + raise ValueError("Unable to generate constructor for empty union") + elif type_def.type.kind == "reference": + type_name = to_upper_camel_case(type_def.name) + ctor_data = types.get_ctor(type_def.type.name) + required = [ + (prop_type, prop_name) + for prop_type, prop_name, optional in ctor_data + if not optional + ] + optional = [ + (prop_type, prop_name) + for prop_type, prop_name, optional in ctor_data + if optional + ] + + ctor_args = [f"{prop_type} {prop_name}" for prop_type, prop_name in required] + ctor_args += [ + f"{prop_type}? {prop_name} = null" for prop_type, prop_name in optional + ] + + base_args = [f"{prop_name}" for _, prop_name in required + optional] + constructor += [ + f"public {type_name}({','.join(ctor_args)}): base({','.join(base_args)}) {{}}", + ] + + 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)", + "{", + "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)", + "{", + ] + 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.");', + "}", + 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"] + get_types_for_usings(code)), 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: + if types.get_by_name(type_def.name): + return + + usings = ["DataContract"] + type_name = get_type_name(type_def.type, types, spec, type_def.name) + class_attributes = [] + if type_def.type.kind == "or": + 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) + lines = namespace_wrapper( + NAMESPACE, + get_usings(usings + get_types_for_usings(inner)), + class_wrapper(type_def, inner, type_name, class_attributes), + ) + types.add_type_info(type_def, type_def.name, lines) + + +def generate_class_from_variant_literals( + literals: List[model.LiteralType], + spec: model.LSPModel, + types: TypeData, + name_context: Optional[str] = None, +) -> str: + name = generate_name(name_context, types) + if types.get_by_name(name): + raise ValueError(f"Name {name} already exists") + + struct = model.Structure( + **{ + "name": name, + "properties": get_properties_from_literals(literals), + } + ) + + lines = generate_code_for_variant_struct(struct, spec, types) + types.add_type_info(struct, struct.name, lines) + return struct.name + + +def get_properties_from_literals(literals: List[model.LiteralType]) -> Dict[str, Any]: + properties = [] + for literal in literals: + assert literal.kind == "literal" + for prop in literal.value.properties: + if prop.name not in [p["name"] for p in properties]: + properties.append( + { + "name": prop.name, + "type": cattrs.unstructure(prop.type), + "optional": has_optional_variant(literals, prop.name), # + } + ) + return properties + + +def generate_code_for_variant_struct( + struct: model.Structure, + spec: model.LSPModel, + types: TypeData, +) -> None: + prop_types = [] + inner = [] + usings = ["DataContract", "JsonConstructor"] + for prop in struct.properties: + prop_code, prop_type = generate_property(prop, spec, types, usings, struct.name) + inner += prop_code + prop_types += [prop_type] + + ctor_data = [] + constructor_args = [] + conditions = [] + for prop, prop_type in zip(struct.properties, prop_types): + name = get_special_case_property_name(to_camel_case(prop.name)) + immutable = prop_type.startswith("ImmutableArray<") or prop_type.startswith( + "ImmutableDictionary<" + ) + constructor_args += [ + f"{prop_type} {name}" if immutable else f"{prop_type}? {name}" + ] + ctor_data = [(prop_type)] + if immutable: + conditions += [f"({name}.IsDefault)"] + else: + conditions += [f"({name} is null)"] + + sig = ", ".join(constructor_args) + types.add_ctor(struct.name, ctor_data) + ctor = [ + f"[JsonConstructor]", + f"public {struct.name}({sig})", + "{", + *indent_lines( + [ + f"if ({'&&'.join(conditions)})", + "{", + *indent_lines( + [ + f'throw new ArgumentException("At least one of the arguments must be non-null");' + ] + ), + "}", + ] + ), + *indent_lines( + [ + f"{to_upper_camel_case(prop.name)} = {get_special_case_property_name(to_camel_case(prop.name))};" + for prop in struct.properties + ] + ), + "}", + ] + + inner = ctor + inner + + return namespace_wrapper( + NAMESPACE, + get_usings(usings + get_types_for_usings(inner)), + class_wrapper(struct, inner, None), + ) + + +def generate_class_from_variant_type_alias( + type_def: model.TypeAlias, + spec: model.LSPModel, + types: TypeData, + name_context: Optional[str] = None, +) -> None: + struct = model.Structure( + **{ + "name": type_def.name, + "properties": get_properties_from_literals(type_def.type.items), + "documentation": type_def.documentation, + "since": type_def.since, + "deprecated": type_def.deprecated, + "proposed": type_def.proposed, + } + ) + + lines = generate_code_for_variant_struct(struct, spec, types) + types.add_type_info(type_def, type_def.name, lines) + + +def has_optional_variant(literals: List[model.LiteralType], property_name: str) -> bool: + count = 0 + optional = False + for literal in literals: + for prop in literal.value.properties: + if prop.name == property_name: + count += 1 + optional = optional or prop.optional + return optional and count == len(literals) + + +def are_variant_literals(literals: List[model.LiteralType]) -> bool: + if all(i.kind == "literal" for i in literals): + return all( + has_optional_variant(literals, prop.name) + for prop in literals[0].value.properties + ) + return False + + +def is_variant_type_alias(type_def: model.TypeAlias) -> bool: + if type_def.type.kind == "or" and all( + i.kind == "literal" for i in type_def.type.items + ): + literals = type_def.type.items + return all( + has_optional_variant(literals, prop.name) + for prop in literals[0].value.properties + ) + return False + + +def copy_struct(struct_def: model.Structure, new_name: str): + converter = cattrs.GenConverter() + obj = converter.unstructure(struct_def, model.Structure) + obj["name"] = new_name + return model.Structure(**obj) + + +def copy_property(prop_def: model.Property): + converter = cattrs.GenConverter() + obj = converter.unstructure(prop_def, model.Property) + return model.Property(**obj) + + +def get_all_extends(struct_def: model.Structure, spec) -> List[model.Structure]: + extends = [] + for extend in struct_def.extends: + extends.append(_get_struct(extend.name, spec)) + for struct in get_all_extends(_get_struct(extend.name, spec), spec): + if not any(struct.name == e.name for e in extends): + extends.append(struct) + return extends + + +def get_all_properties(struct: model.Structure, spec) -> List[model.Structure]: + properties = [] + for prop in struct.properties: + properties.append(copy_property(prop)) + + for extend in get_all_extends(struct, spec): + for prop in get_all_properties(extend, spec): + if not any(prop.name == p.name for p in properties): + properties.append(copy_property(prop)) + + if not all(mixin.kind == "reference" for mixin in struct.mixins): + raise ValueError(f"Struct {struct.name} has non-reference mixins") + for mixin in [_get_struct(mixin.name, spec) for mixin in struct.mixins]: + for prop in get_all_properties(mixin, spec): + if not any(prop.name == p.name for p in properties): + properties.append(copy_property(prop)) + + return properties + + +def generate_code_for_request(request: model.Request): + lines = get_doc(request.documentation) + generate_extras(request) + lines.append( + f'public static string {lsp_method_to_name(request.method)} {{ get; }} = "{request.method}";' + ) + return lines + + +def generate_code_for_notification(notify: model.Notification): + lines = get_doc(notify.documentation) + generate_extras(notify) + lines.append( + f'public static string {lsp_method_to_name(notify.method)} {{ get; }} = "{notify.method}";' + ) + return lines + + +def generate_request_notification_methods(spec: model.LSPModel, types: TypeData): + inner_lines = [] + for request in spec.requests: + inner_lines += generate_code_for_request(request) + + for notification in spec.notifications: + inner_lines += generate_code_for_notification(notification) + + lines = namespace_wrapper( + NAMESPACE, + get_usings(["System"] + get_types_for_usings(inner_lines)), + ["public static class LSPMethods", "{", *indent_lines(inner_lines), "}"], + ) + enum_type = model.Enum( + **{ + "name": "LSPMethods", + "type": {"kind": "base", "name": "string"}, + "values": [], + "documentation": "LSP methods as defined in the LSP spec", + } + ) + types.add_type_info(enum_type, "LSPMethods", lines) + + +def get_message_template( + obj: Union[model.Request, model.Notification], + is_request: bool, +) -> model.Structure: + text = "Request" if is_request else "Notification" + properties = [ + { + "name": "jsonrpc", + "type": {"kind": "stringLiteral", "value": "2.0"}, + "documentation": "The jsonrpc version.", + } + ] + if is_request: + properties += [ + { + "name": "id", + "type": { + "kind": "or", + "items": [ + {"kind": "base", "name": "string"}, + {"kind": "base", "name": "integer"}, + ], + }, + "documentation": f"The {text} id.", + } + ] + properties += [ + { + "name": "method", + "type": {"kind": "base", "name": "string"}, + "documentation": f"The {text} method.", + }, + ] + if obj.params: + properties.append( + { + "name": "params", + "type": cattrs.unstructure(obj.params), + "documentation": f"The {text} parameters.", + } + ) + else: + properties.append( + { + "name": "params", + "type": {"kind": "reference", "name": "LSPAny"}, + "documentation": f"The {text} parameters.", + "optional": True, + } + ) + + class_template = { + "name": f"{lsp_method_to_name(obj.method)}{text}", + "properties": properties, + "documentation": obj.documentation, + "since": obj.since, + "deprecated": obj.deprecated, + "proposed": obj.proposed, + } + 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, + types: TypeData, +) -> model.Structure: + if obj.registrationOptions and obj.registrationOptions.kind != "reference": + if obj.registrationOptions.kind == "and": + structs = [_get_struct(s.name, spec) for s in obj.registrationOptions.items] + properties = [] + for struct in structs: + properties += get_all_properties(struct, spec) + + class_template = { + "name": f"{lsp_method_to_name(obj.method)}RegistrationOptions", + "properties": [ + cattrs.unstructure(p, model.Property) for p in properties + ], + } + return model.Structure(**class_template) + else: + raise ValueError( + f"Unexpected registrationOptions type: {obj.registrationOptions.type.kind}" + ) + return None + + +def generate_all_classes(spec: model.LSPModel, types: TypeData): + for struct in spec.structures: + generate_class_from_struct(struct, spec, types) + + for type_alias in spec.typeAliases: + if is_variant_type_alias(type_alias): + generate_class_from_variant_type_alias(type_alias, spec, types) + else: + generate_class_from_type_alias(type_alias, spec, types) + + 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, + spec, + types, + ( + f"IRequest<{get_type_name(request.params, types, spec)}>" + if request.params + else "IRequest<LSPAny?>" + ), + [ + 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: + generate_class_from_struct( + registration_options, + spec, + types, + ) + + for notification in spec.notifications: + struct = get_message_template(notification, is_request=False) + generate_class_from_struct( + struct, + spec, + types, + ( + f"INotification<{get_type_name(notification.params, types, spec)}>" + if notification.params + else "INotification<LSPAny>" + ), + [ + f"[Direction(MessageDirection.{to_upper_camel_case(request.messageDirection)})]", + ], + ) + registration_options = get_registration_options_template( + notification, spec, types + ) + if registration_options: + generate_class_from_struct( + registration_options, + spec, + types, + ) diff --git a/generator/plugins/dotnet/dotnet_commons.py b/generator/plugins/dotnet/dotnet_commons.py new file mode 100644 index 0000000..9c21eca --- /dev/null +++ b/generator/plugins/dotnet/dotnet_commons.py @@ -0,0 +1,60 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import Dict, List, Tuple, Union + +from generator import model + +TypesWithId = Union[ + model.Request, + model.TypeAlias, + model.Enum, + model.Structure, + model.Notification, + model.LiteralType, + model.ReferenceType, + model.ReferenceMapKeyType, + model.Property, + model.EnumItem, +] + + +class TypeData: + def __init__(self) -> None: + self._id_data: Dict[str, Tuple[str, TypesWithId, List[str]]] = {} + self._ctor_data: Dict[str, Tuple[str, str]] = {} + + def add_type_info( + self, + type_def: TypesWithId, + type_name: str, + impl: List[str], + ) -> None: + if type_def.id_ in self._id_data: + raise Exception(f"Duplicate id {type_def.id_} for type {type_name}") + self._id_data[type_def.id_] = (type_name, type_def, impl) + + def has_id( + self, + type_def: TypesWithId, + ) -> bool: + return type_def.id_ in self._id_data + + def has_name(self, type_name: str) -> bool: + return any(type_name == name for name, _, _ in self._id_data.values()) + + def get_by_name(self, type_name: str) -> List[TypesWithId]: + return [ + type_def + for name, type_def, _ in self._id_data.values() + if name == type_name + ] + + def get_all(self) -> List[Tuple[str, List[str]]]: + return [(name, lines) for name, _, lines in self._id_data.values()] + + def add_ctor(self, type_name: str, ctor: Tuple[str, str, bool]) -> None: + self._ctor_data[type_name] = ctor + + def get_ctor(self, type_name: str) -> Tuple[str, str, bool]: + return self._ctor_data[type_name] diff --git a/generator/plugins/dotnet/dotnet_constants.py b/generator/plugins/dotnet/dotnet_constants.py new file mode 100644 index 0000000..f66c96b --- /dev/null +++ b/generator/plugins/dotnet/dotnet_constants.py @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +NAMESPACE = "Microsoft.LanguageServer.Protocol" +PACKAGE_DIR_NAME = "lsprotocol" diff --git a/generator/plugins/dotnet/dotnet_enums.py b/generator/plugins/dotnet/dotnet_enums.py new file mode 100644 index 0000000..c5e7a6c --- /dev/null +++ b/generator/plugins/dotnet/dotnet_enums.py @@ -0,0 +1,51 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import Dict, List, Union + +from generator import model + +from .dotnet_commons import TypeData +from .dotnet_constants import NAMESPACE +from .dotnet_helpers import ( + indent_lines, + lines_to_doc_comments, + namespace_wrapper, + to_upper_camel_case, +) + + +def generate_enums(spec: model.LSPModel, types: TypeData) -> None: + """Generate the code for the given spec.""" + for enum_def in spec.enumerations: + types.add_type_info(enum_def, enum_def.name, generate_enum(enum_def)) + + +def _get_enum_doc(enum: Union[model.Enum, model.EnumItem]) -> List[str]: + doc = enum.documentation.splitlines(keepends=False) if enum.documentation else [] + return lines_to_doc_comments(doc) + + +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;"] + 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: + name = to_upper_camel_case(item.name) + inner = _get_enum_doc(item) + if use_enum_member: + inner += [f'[EnumMember(Value = "{item.value}")]{name},'] + else: + inner += [f"{name} = {item.value},"] + lines += indent_lines(inner) + [""] + + lines += ["}"] + + return namespace_wrapper(NAMESPACE, (imports if use_enum_member else []), lines) diff --git a/generator/plugins/dotnet/dotnet_helpers.py b/generator/plugins/dotnet/dotnet_helpers.py new file mode 100644 index 0000000..2eb357a --- /dev/null +++ b/generator/plugins/dotnet/dotnet_helpers.py @@ -0,0 +1,215 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import re +from typing import List, Optional, Union + +from generator import model + +BASIC_LINK_RE = re.compile(r"{@link +(\w+) ([\w ]+)}") +BASIC_LINK_RE2 = re.compile(r"{@link +(\w+)\.(\w+) ([\w \.`]+)}") +BASIC_LINK_RE3 = re.compile(r"{@link +(\w+)}") +BASIC_LINK_RE4 = re.compile(r"{@link +(\w+)\.(\w+)}") +PARTS_RE = re.compile(r"(([a-z0-9])([A-Z]))") + + +def _fix_links(line: str) -> str: + line = BASIC_LINK_RE.sub(r'<see cref="\1">\2</see>', line) + line = BASIC_LINK_RE2.sub(r'<see cref="\1.\2">\3</see>', line) + line = BASIC_LINK_RE3.sub(r'<see cref="\1" />', line) + line = BASIC_LINK_RE4.sub(r'<see cref="\1.\2" />', line) + return line + + +def lines_to_doc_comments(lines: List[str]) -> List[str]: + if not lines: + return [] + + return ( + ["/// <summary>"] + + [f"/// {_fix_links(line)}" for line in lines if not line.startswith("@")] + + ["/// </summary>"] + ) + + +def get_parts(name: str) -> List[str]: + name = name.replace("_", " ") + return PARTS_RE.sub(r"\2 \3", name).split() + + +def to_camel_case(name: str) -> str: + parts = get_parts(name) + return parts[0] + "".join([p.capitalize() for p in parts[1:]]) + + +def to_upper_camel_case(name: str) -> str: + return "".join([c.capitalize() for c in get_parts(name)]) + + +def lsp_method_to_name(method: str) -> str: + if method.startswith("$"): + method = method[1:] + method = method.replace("/", "_") + return to_upper_camel_case(method) + + +def file_header() -> List[str]: + return [ + "// Copyright (c) Microsoft Corporation. All rights reserved.", + "// Licensed under the MIT License.", + "// ", + "// THIS FILE IS AUTOGENERATED, DO NOT MODIFY IT", + "", + ] + + +def namespace_wrapper( + namespace: str, imports: List[str], lines: List[str] +) -> List[str]: + indent = " " * 4 + return ( + file_header() + + imports + + [""] + + ["namespace " + namespace + " {"] + + [(f"{indent}{line}" if line else line) for line in lines] + + ["}", ""] + ) + + +def get_doc(doc: Optional[str]) -> str: + if doc: + return lines_to_doc_comments(doc.splitlines(keepends=False)) + return [] + + +def get_special_case_class_name(name: str) -> str: + # This is because C# does not allow class name and property name to be the same. + # public class Command{ public string Command { get; set; }} is not valid. + if name == "Command": + return "CommandAction" + return name + + +def get_special_case_property_name(name: str) -> str: + if name == "string": + return "stringValue" + if name == "int": + return "intValue" + if name == "event": + return "eventArgs" + if name == "params": + return "paramsValue" + return name + + +def class_wrapper( + type_def: Union[model.Structure, model.Notification, model.Request], + inner: List[str], + derived: Optional[str] = None, + class_attributes: Optional[List[str]] = None, + is_record=True, +) -> List[str]: + if hasattr(type_def, "name"): + name = get_special_case_class_name(type_def.name) + else: + raise ValueError(f"Unknown type: {type_def}") + + rec_or_cls = "record" if is_record else "class" + lines = ( + get_doc(type_def.documentation) + + generate_extras(type_def) + + (class_attributes if class_attributes else []) + + [ + "[DataContract]", + f"public {rec_or_cls} {name}: {derived}" + if derived + else f"public {rec_or_cls} {name}", + "{", + ] + ) + lines += indent_lines(inner) + lines += ["}", ""] + return lines + + +def property_wrapper(prop_def: model.Property, content: List[str]) -> List[str]: + lines = (get_doc(prop_def.documentation) + generate_extras(prop_def) + content,) + lines += indent_lines(content) + return lines + + +def indent_lines(lines: List[str], indent: str = " " * 4) -> List[str]: + return [(f"{indent}{line}" if line else line) for line in lines] + + +def cleanup_str(text: str) -> str: + return text.replace("\r", "").replace("\n", "") + + +def get_deprecated(text: Optional[str]) -> Optional[str]: + if not text: + return None + + lines = text.splitlines(keepends=False) + for line in lines: + if line.startswith("@deprecated"): + return line.replace("@deprecated", "").strip() + return None + + +def generate_extras( + type_def: Union[ + model.Enum, + model.EnumItem, + model.Property, + model.TypeAlias, + model.Structure, + model.Request, + model.Notification, + ] +) -> List[str]: + deprecated = get_deprecated(type_def.documentation) + extras = [] + if type_def.deprecated: + extras += [f'[Obsolete("{cleanup_str(type_def.deprecated)}")]'] + elif deprecated: + extras += [f'[Obsolete("{cleanup_str(deprecated)}")]'] + if type_def.proposed: + extras += [f"[Proposed]"] + if type_def.since: + extras += [f'[Since("{cleanup_str(type_def.since)}")]'] + + if hasattr(type_def, "messageDirection"): + if type_def.since: + extras += [ + f"[Direction(MessageDirection.{to_upper_camel_case(type_def.messageDirection)})]" + ] + + return extras + + +def get_usings(types: List[str]) -> List[str]: + usings = [] + + for t in ["DataMember", "DataContract"]: + if t in types: + usings.append("using System.Runtime.Serialization;") + + for t in ["JsonConverter", "JsonConstructor", "JsonProperty", "NullValueHandling"]: + if t in types: + usings.append("using Newtonsoft.Json;") + + for t in ["JToken", "JObject", "JArray"]: + if t in types: + usings.append("using Newtonsoft.Json.Linq;") + + for t in ["List", "Dictionary"]: + if t in types: + usings.append("using System.Collections.Generic;") + + for t in ["ImmutableArray", "ImmutableDictionary"]: + if t in types: + usings.append("using System.Collections.Immutable;") + + return sorted(list(set(usings))) diff --git a/generator/plugins/dotnet/dotnet_special_classes.py b/generator/plugins/dotnet/dotnet_special_classes.py new file mode 100644 index 0000000..a656b6d --- /dev/null +++ b/generator/plugins/dotnet/dotnet_special_classes.py @@ -0,0 +1,158 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import Dict, List, Union + +from generator import model + +from .dotnet_commons import TypeData +from .dotnet_constants import NAMESPACE +from .dotnet_helpers import class_wrapper, get_usings, namespace_wrapper + +SPECIAL_CLASSES = [ + "LSPObject", + "LSPAny", + "LSPArray", + "ChangeAnnotationIdentifier", + "Pattern", + "DocumentSelector", + "InitializedParams", +] + + +def generate_special_classes(spec: model.LSPModel, types: TypeData) -> None: + """Generate code for special classes in LSP.""" + for special_class in SPECIAL_CLASSES: + for class_def in spec.structures + spec.typeAliases: + if class_def.name == special_class: + generate_special_class(class_def, spec, types) + + +def generate_special_class( + type_def: Union[model.Structure, model.TypeAlias], + spec: model.LSPModel, + types: TypeData, +) -> Dict[str, str]: + """Generate code for a special class.""" + lines: List[str] = [] + + if type_def.name == "LSPObject": + lines = namespace_wrapper( + NAMESPACE, + get_usings(["Dictionary", "DataContract", "JsonConverter"]), + class_wrapper( + type_def, + ["public LSPObject(Dictionary<string, object?> value):base(value){}"], + "Dictionary<string, object?>", + ["[JsonConverter(typeof(CustomObjectConverter<LSPObject>))]"], + is_record=False, + ), + ) + if type_def.name == "InitializedParams": + lines = namespace_wrapper( + NAMESPACE, + get_usings(["Dictionary", "DataContract", "JsonConverter"]), + class_wrapper( + type_def, + [ + "public InitializedParams(Dictionary<string, object?> value):base(value){}" + ], + "Dictionary<string, object?>", + ["[JsonConverter(typeof(CustomObjectConverter<InitializedParams>))]"], + is_record=False, + ), + ) + if type_def.name == "LSPAny": + lines = namespace_wrapper( + NAMESPACE, + 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, + ["public LSPArray(List<object> value):base(value){}"], + "List<object>", + is_record=False, + ), + ) + + if type_def.name == "Pattern": + inner = [ + "private string pattern;", + "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, + get_usings(["JsonConverter", "DataContract"]), + class_wrapper( + type_def, + inner, + None, + [f"[JsonConverter(typeof(CustomStringConverter<{type_def.name}>))]"], + ), + ) + + if type_def.name == "ChangeAnnotationIdentifier": + inner = [ + "private string identifier;", + "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, + get_usings(["JsonConverter", "DataContract"]), + class_wrapper( + type_def, + inner, + None, + [f"[JsonConverter(typeof(CustomStringConverter<{type_def.name}>))]"], + ), + ) + + if type_def.name == "DocumentSelector": + inner = [ + "private DocumentFilter[] Filters { get; set; }", + "public DocumentSelector(params DocumentFilter[] filters)", + "{", + " Filters = filters ?? Array.Empty<DocumentFilter>();", + "}", + "public DocumentFilter this[int index]", + "{", + " get { return Filters[index]; }", + " set { Filters[index] = value; }", + "}", + "public int Length => Filters.Length;", + "public static implicit operator DocumentSelector(DocumentFilter[] filters) => new(filters);", + "public static implicit operator DocumentFilter[](DocumentSelector selector) => selector.Filters;", + "public IEnumerator<DocumentFilter> GetEnumerator() => ((IEnumerable<DocumentFilter>)Filters).GetEnumerator();", + "System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => Filters.GetEnumerator();", + ] + lines = namespace_wrapper( + NAMESPACE, + get_usings(["JsonConverter", "DataContract"]), + class_wrapper( + type_def, + inner, + "IEnumerable<DocumentFilter>", + [f"[JsonConverter(typeof(DocumentSelectorConverter))]"], + ), + ) + + types.add_type_info(type_def, type_def.name, lines) diff --git a/generator/plugins/dotnet/dotnet_utils.py b/generator/plugins/dotnet/dotnet_utils.py new file mode 100644 index 0000000..4510527 --- /dev/null +++ b/generator/plugins/dotnet/dotnet_utils.py @@ -0,0 +1,58 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import logging +import os +import pathlib +import subprocess +from typing import Dict, List + +import generator.model as model + +from .dotnet_classes import generate_all_classes +from .dotnet_commons import TypeData +from .dotnet_constants import NAMESPACE, PACKAGE_DIR_NAME +from .dotnet_enums import generate_enums +from .dotnet_helpers import namespace_wrapper +from .dotnet_special_classes import generate_special_classes + +LOGGER = logging.getLogger("dotnet") + + +def generate_from_spec(spec: model.LSPModel, output_dir: str) -> None: + """Generate the code for the given spec.""" + output_path = pathlib.Path(output_dir, PACKAGE_DIR_NAME) + if not output_path.exists(): + output_path.mkdir(parents=True, exist_ok=True) + + cleanup(output_path) + copy_custom_classes(output_path) + + LOGGER.info("Generating code in C#") + types = TypeData() + generate_package_code(spec, types) + + for name, lines in types.get_all(): + file_name = f"{name}.cs" + (output_path / file_name).write_text("\n".join(lines), encoding="utf-8") + + +def generate_package_code(spec: model.LSPModel, types: TypeData) -> Dict[str, str]: + generate_enums(spec, types) + generate_special_classes(spec, types) + generate_all_classes(spec, types) + + +def cleanup(output_path: pathlib.Path) -> None: + """Cleanup the generated C# files.""" + for file in output_path.glob("*.cs"): + file.unlink() + + +def copy_custom_classes(output_path: pathlib.Path) -> None: + """Copy the custom classes to the output directory.""" + custom = pathlib.Path(__file__).parent / "custom" + for file in custom.glob("*.cs"): + lines = file.read_text(encoding="utf-8").splitlines() + lines = namespace_wrapper(NAMESPACE, [], lines) + (output_path / file.name).write_text("\n".join(lines), encoding="utf-8") |