diff options
Diffstat (limited to 'generator/plugins/dotnet/dotnet_classes.py')
-rw-r--r-- | generator/plugins/dotnet/dotnet_classes.py | 1024 |
1 files changed, 1024 insertions, 0 deletions
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, + ) |