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/rust | |
parent | Initial commit. (diff) | |
download | lsprotocol-upstream.tar.xz lsprotocol-upstream.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/rust')
-rw-r--r-- | generator/plugins/rust/__init__.py | 4 | ||||
-rw-r--r-- | generator/plugins/rust/rust_commons.py | 681 | ||||
-rw-r--r-- | generator/plugins/rust/rust_constants.py | 2 | ||||
-rw-r--r-- | generator/plugins/rust/rust_enum.py | 51 | ||||
-rw-r--r-- | generator/plugins/rust/rust_file_header.py | 15 | ||||
-rw-r--r-- | generator/plugins/rust/rust_lang_utils.py | 73 | ||||
-rw-r--r-- | generator/plugins/rust/rust_structs.py | 460 | ||||
-rw-r--r-- | generator/plugins/rust/rust_utils.py | 68 |
8 files changed, 1354 insertions, 0 deletions
diff --git a/generator/plugins/rust/__init__.py b/generator/plugins/rust/__init__.py new file mode 100644 index 0000000..cbc2c00 --- /dev/null +++ b/generator/plugins/rust/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .rust_utils import generate_from_spec as generate diff --git a/generator/plugins/rust/rust_commons.py b/generator/plugins/rust/rust_commons.py new file mode 100644 index 0000000..4022e27 --- /dev/null +++ b/generator/plugins/rust/rust_commons.py @@ -0,0 +1,681 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import Dict, List, Optional, Tuple, Union + +from generator import model + +from .rust_lang_utils import ( + get_parts, + indent_lines, + lines_to_doc_comments, + to_snake_case, + to_upper_camel_case, +) + +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], + ], + ] = {} + + 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_name == name for name, _, _ in self._id_data.values()] + + def get_lines(self): + lines = [] + for _, _, impl in self._id_data.values(): + lines += impl + ["", ""] + return lines + + +def generate_custom_enum(type_data: TypeData) -> None: + type_data.add_type_info( + model.ReferenceType(kind="reference", name="CustomStringEnum"), + "CustomStringEnum", + [ + "/// This type allows extending any string enum to support custom values.", + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum CustomStringEnum<T> {", + " /// The value is one of the known enum values.", + " Known(T),", + " /// The value is custom.", + " Custom(String),", + "}", + "", + ], + ) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="CustomIntEnum"), + "CustomIntEnum", + [ + "/// This type allows extending any integer enum to support custom values.", + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum CustomIntEnum<T> {", + " /// The value is one of the known enum values.", + " Known(T),", + " /// The value is custom.", + " Custom(i32),", + "}", + "", + ], + ) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="OR2"), + "OR2", + [ + "/// This allows a field to have two types.", + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum OR2<T, U> {", + " T(T),", + " U(U),", + "}", + "", + ], + ) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="OR3"), + "OR3", + [ + "/// This allows a field to have three types.", + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum OR3<T, U, V> {", + " T(T),", + " U(U),", + " V(V),", + "}", + "", + ], + ) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="OR4"), + "OR4", + [ + "/// This allows a field to have four types.", + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum OR4<T, U, V, W> {", + " T(T),", + " U(U),", + " V(V),", + " W(W),", + "}", + "", + ], + ) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="OR5"), + "OR5", + [ + "/// This allows a field to have five types.", + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum OR5<T, U, V, W, X> {", + " T(T),", + " U(U),", + " V(V),", + " W(W),", + " X(X),", + "}", + "", + ], + ) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="OR6"), + "OR6", + [ + "/// This allows a field to have six types.", + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum OR6<T, U, V, W, X, Y> {", + " T(T),", + " U(U),", + " V(V),", + " W(W),", + " X(X),", + " Y(Y),", + "}", + "", + ], + ) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="OR7"), + "OR7", + [ + "/// This allows a field to have seven types.", + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum OR7<T, U, V, W, X, Y, Z> {", + " T(T),", + " U(U),", + " V(V),", + " W(W),", + " X(X),", + " Y(Y),", + " Z(Z),", + "}", + "", + ], + ) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="LSPNull"), + "LSPNull", + [ + "/// This allows a field to always have null or empty value.", + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum LSPNull {", + " None,", + "}", + "", + ], + ) + + +def get_definition( + name: str, spec: model.LSPModel +) -> Optional[Union[model.TypeAlias, model.Structure]]: + for type_def in spec.typeAliases + spec.structures: + if type_def.name == name: + return type_def + return None + + +def generate_special_types(model: model.LSPModel, types: TypeData) -> None: + special_types = [ + get_definition("LSPAny", model), + get_definition("LSPObject", model), + get_definition("LSPArray", model), + get_definition("SelectionRange", model), + ] + + for type_def in special_types: + if type_def: + doc = ( + type_def.documentation.splitlines(keepends=False) + if type_def.documentation + else [] + ) + lines = lines_to_doc_comments(doc) + lines += generate_extras(type_def) + + if type_def.name == "LSPAny": + lines += [ + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum LSPAny {", + " String(String),", + " Integer(i32),", + " UInteger(u32),", + " Decimal(Decimal),", + " Boolean(bool),", + " Object(LSPObject),", + " Array(LSPArray),", + " Null,", + "}", + ] + elif type_def.name == "LSPObject": + lines += ["type LSPObject = serde_json::Value;"] + elif type_def.name == "LSPArray": + lines += ["type LSPArray = Vec<LSPAny>;"] + elif type_def.name == "SelectionRange": + lines += [ + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "pub struct SelectionRange {", + ] + for property in type_def.properties: + doc = ( + property.documentation.splitlines(keepends=False) + if property.documentation + else [] + ) + lines += lines_to_doc_comments(doc) + lines += generate_extras(property) + prop_name = to_snake_case(property.name) + prop_type = get_type_name( + property.type, types, model, property.optional + ) + if "SelectionRange" in prop_type: + prop_type = prop_type.replace( + "SelectionRange", "Box<SelectionRange>" + ) + lines += [f"pub {prop_name}: {prop_type},"] + lines += [""] + lines += ["}"] + lines += [""] + types.add_type_info(type_def, type_def.name, lines) + + +def fix_lsp_method_name(name: str) -> str: + if name.startswith("$/"): + name = name[2:] + return to_upper_camel_case(name.replace("/", "_")) + + +def generate_special_enum(enum_name: str, items: List[str]) -> Dict[str, List[str]]: + lines = [ + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + f"pub enum {enum_name}" "{", + ] + for item in items: + lines += indent_lines( + [ + f'#[serde(rename = "{item}")]', + f"{fix_lsp_method_name(item)},", + ] + ) + lines += ["}"] + return lines + + +def generate_extra_types(spec: model.LSPModel, type_data: TypeData) -> None: + type_data.add_type_info( + model.ReferenceType(kind="reference", name="LSPRequestMethods"), + "LSPRequestMethods", + generate_special_enum("LSPRequestMethods", [m.method for m in spec.requests]), + ) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="LSPNotificationMethods"), + "LSPNotificationMethods", + generate_special_enum( + "LSPNotificationMethods", [m.method for m in spec.notifications] + ), + ) + + direction = set([m.messageDirection for m in (spec.requests + spec.notifications)]) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="MessageDirection"), + "MessageDirection", + generate_special_enum("MessageDirection", direction), + ) + + +def generate_commons( + model: model.LSPModel, type_data: TypeData +) -> Dict[str, List[str]]: + generate_custom_enum(type_data) + generate_special_types(model, type_data) + generate_extra_types(model, type_data) + + +def lsp_to_base_types(lsp_type: model.BaseType): + if lsp_type.name in ["string", "DocumentUri", "URI", "RegExp"]: + return "String" + elif lsp_type.name in ["decimal"]: + return "Decimal" + elif lsp_type.name in ["integer"]: + return "i32" + elif lsp_type.name in ["uinteger"]: + return "u32" + elif lsp_type.name in ["boolean"]: + return "bool" + + # null should be handled by the caller as an Option<> type + raise ValueError(f"Unknown base type: {lsp_type.name}") + + +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_from_name( + name: str, spec: model.LSPModel +) -> Optional[Union[model.Structure, model.Enum, model.TypeAlias]]: + for some in spec.enumerations + spec.structures + spec.typeAliases: + if some.name == name: + return some + return None + + +def get_extended_properties( + struct_def: model.Structure, spec: model.LSPModel +) -> List[model.Property]: + properties = [p for p in struct_def.properties] + for t in struct_def.extends + struct_def.mixins: + if t.kind == "reference": + s = get_from_name(t.name, spec) + if s: + properties += [p for p in s.properties] + elif t.kind == "literal": + properties += [p for p in t.value.properties] + else: + raise ValueError(f"Unhandled extension type or mixin type: {t.kind}") + unique_props = [] + for p in properties: + if not any((p.name == u.name) for u in unique_props): + unique_props.append(p) + return sorted(unique_props, key=lambda p: p.name) + + +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 generate_or_type( + type_def: model.LSP_TYPE_SPEC, + types: TypeData, + spec: model.LSPModel, + optional: Optional[bool] = None, + name_context: Optional[str] = None, +) -> str: + pass + + +def generate_and_type( + type_def: model.LSP_TYPE_SPEC, + types: TypeData, + spec: model.LSPModel, + optional: Optional[bool] = None, + name_context: Optional[str] = None, +) -> str: + pass + + +def get_type_name( + type_def: model.LSP_TYPE_SPEC, + types: TypeData, + spec: model.LSPModel, + optional: Optional[bool] = None, + name_context: Optional[str] = None, +) -> str: + 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"CustomStringEnum<{enum_def.name}>" + elif _is_int_enum(enum_def): + name = f"CustomIntEnum<{enum_def.name}>" + else: + name = type_def.name + elif type_def.kind == "array": + name = f"Vec<{get_type_name(type_def.element, types, spec)}>" + elif type_def.kind == "map": + key_type = get_type_name(type_def.key, types, spec) + value_type = get_type_name(type_def.value, types, spec) + name = f"HashMap<{key_type}, {value_type}>" + elif type_def.kind == "base": + name = lsp_to_base_types(type_def) + elif type_def.kind == "or": + sub_set_items = [ + sub_spec + for sub_spec in type_def.items + if not (sub_spec.kind == "base" and sub_spec.name == "null") + ] + sub_types = [get_type_name(sub_spec, types, spec) for sub_spec in sub_set_items] + sub_types_str = ", ".join(sub_types) + if len(sub_types) >= 2: + name = f"OR{len(sub_types)}<{sub_types_str}>" + elif len(sub_types) == 1: + name = sub_types[0] + else: + raise ValueError( + f"OR type with more than out of range count of subtypes: {type_def}" + ) + optional = optional or is_special(type_def) + print(f"Option<{name}>" if optional else name) + elif type_def.kind == "literal": + name = generate_literal_struct_type(type_def, types, spec, name_context) + elif type_def.kind == "stringLiteral": + name = "String" + # This type in rust requires a custom deserializer that fails if the value is not + # one of the allowed values. This should be handled by the caller. This cannot be + # handled here because all this does is handle type names. + elif type_def.kind == "tuple": + optional = optional or is_special(type_def) + sub_set_items = [ + sub_spec + for sub_spec in type_def.items + if not (sub_spec.kind == "base" and sub_spec.name == "null") + ] + sub_types = [get_type_name(sub_spec, types, spec) for sub_spec in sub_set_items] + sub_types_str = ", ".join(sub_types) + if len(sub_types) >= 2: + name = f"({sub_types_str})" + elif len(sub_types) == 1: + name = sub_types[0] + else: + raise ValueError(f"Invalid number of items for tuple: {type_def}") + else: + raise ValueError(f"Unknown type kind: {type_def.kind}") + + return f"Option<{name}>" if optional else name + + +def is_special(type_def: model.LSP_TYPE_SPEC) -> bool: + if type_def.kind in ["or", "tuple"]: + for item in type_def.items: + if item.kind == "base" and item.name == "null": + return True + return False + + +def is_special_property(prop_def: model.Property) -> bool: + return is_special(prop_def.type) + + +def is_string_literal_property(prop_def: model.Property) -> bool: + return prop_def.type.kind == "stringLiteral" + + +def generate_literal_struct_name( + type_def: model.LiteralType, + types: TypeData, + spec: model.LSPModel, + name_context: Optional[str] = None, +) -> str: + ignore_list = ["Struct", "Type", "Kind", "Options", "Params", "Result", "Options"] + + initial_parts = ["Struct"] + if name_context: + initial_parts += get_parts(name_context) + + optional_props = [p for p in type_def.value.properties if p.optional] + required_props = [p for p in type_def.value.properties if not p.optional] + + required_parts = [] + for property in required_props: + for p in get_parts(property.name): + if p not in (ignore_list + required_parts): + required_parts.append(p) + + optional = ( + ["Options"] if len(optional_props) == len(type_def.value.properties) else [] + ) + + name_parts = initial_parts + name = to_upper_camel_case("_".join(name_parts)) + + all_ignore = all(n in ignore_list for n in name_parts) + if types.has_name(name) or all_ignore: + parts = [] + + for r in required_parts: + parts.append(r) + name = to_upper_camel_case("_".join(initial_parts + parts + optional)) + if not types.has_name(name): + return name + + for i in range(1, 100): + end = [f"{i}"] if i > 1 else [] + name = to_upper_camel_case( + "_".join(initial_parts + required_parts + optional + end) + ) + if not types.has_name(name): + return name + return name + + +def _get_doc(doc: Optional[str]) -> str: + if doc: + return lines_to_doc_comments(doc.splitlines(keepends=False)) + return [] + + +def generate_property( + prop_def: model.Property, types: TypeData, spec: model.LSPModel +) -> str: + prop_name = to_snake_case(prop_def.name) + prop_type = get_type_name( + prop_def.type, types, spec, prop_def.optional, prop_def.name + ) + optional = ( + [f'#[serde(skip_serializing_if = "Option::is_none")]'] + if is_special_property(prop_def) and not prop_def.optional + else [] + ) + + if prop_name in ["type"]: + prop_name = f"{prop_name}_" + if optional: + optional = [ + f'#[serde(rename = "{prop_def.name}", skip_serializing_if = "Option::is_none")]' + ] + else: + optional = [f'#[serde(rename = "{prop_def.name}")]'] + + return ( + _get_doc(prop_def.documentation) + + generate_extras(prop_def) + + optional + + [f"pub {prop_name}: {prop_type},"] + + [""] + ) + + +def get_message_type_name(type_def: Union[model.Notification, model.Request]) -> str: + name = fix_lsp_method_name(type_def.method) + if isinstance(type_def, model.Notification): + return f"{name}Notification" + return f"{name}Request" + + +def struct_wrapper( + type_def: Union[model.Structure, model.Notification, model.Request], + inner: List[str], +) -> List[str]: + if hasattr(type_def, "name"): + name = type_def.name + else: + name = get_message_type_name(type_def) + lines = ( + _get_doc(type_def.documentation) + + generate_extras(type_def) + + [ + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + '#[serde(rename_all = "camelCase")]', + f"pub struct {name}", + "{", + ] + ) + lines += indent_lines(inner) + lines += ["}", ""] + return lines + + +def type_alias_wrapper(type_def: model.TypeAlias, inner: List[str]) -> List[str]: + lines = ( + _get_doc(type_def.documentation) + + generate_extras(type_def) + + [ + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + f"pub enum {type_def.name}", + "{", + ] + ) + lines += indent_lines(inner) + lines += ["}", ""] + return lines + + +def generate_literal_struct_type( + type_def: model.LiteralType, + types: TypeData, + spec: model.LSPModel, + name_context: Optional[str] = None, +) -> None: + if len(type_def.value.properties) == 0: + return "LSPObject" + + if types.has_id(type_def): + return type_def.name + + type_def.name = generate_literal_struct_name(type_def, types, spec, name_context) + + inner = [] + for prop_def in type_def.value.properties: + inner += generate_property(prop_def, types, spec) + + lines = struct_wrapper(type_def, inner) + types.add_type_info(type_def, type_def.name, lines) + return type_def.name + + +def generate_extras( + type_def: Union[ + model.Enum, model.EnumItem, model.Property, model.TypeAlias, model.Structure + ] +) -> List[str]: + extras = [] + if type_def.deprecated: + extras = ["#[deprecated]"] + elif type_def.proposed: + if type_def.since: + extras = [f'#[cfg(feature = "proposed", since = "{type_def.since}")]'] + else: + extras = [f'#[cfg(feature = "proposed")]'] + # else: + # if type_def.since: + # extras = [f'#[cfg(feature = "stable", since = "{type_def.since}")]'] + # else: + # extras = [f'#[cfg(feature = "stable")]'] + return extras diff --git a/generator/plugins/rust/rust_constants.py b/generator/plugins/rust/rust_constants.py new file mode 100644 index 0000000..5b7f7a9 --- /dev/null +++ b/generator/plugins/rust/rust_constants.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/generator/plugins/rust/rust_enum.py b/generator/plugins/rust/rust_enum.py new file mode 100644 index 0000000..5773d12 --- /dev/null +++ b/generator/plugins/rust/rust_enum.py @@ -0,0 +1,51 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import List, Union + +import generator.model as model + +from .rust_commons import TypeData, generate_extras +from .rust_lang_utils import indent_lines, lines_to_doc_comments, to_upper_camel_case + + +def _get_enum_docs(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, types: TypeData) -> None: + is_int = all(isinstance(item.value, int) for item in enum.values) + + lines = ( + _get_enum_docs(enum) + + generate_extras(enum) + + [ + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + f"pub enum {enum.name} " "{", + ] + ) + + for item in enum.values: + if is_int: + field = [ + f"{to_upper_camel_case(item.name)} = {item.value},", + ] + else: + field = [ + f'#[serde(rename = "{item.value}")]', + f"{to_upper_camel_case(item.name)},", + ] + + lines += indent_lines( + _get_enum_docs(item) + generate_extras(item) + field + [""] + ) + + lines += ["}"] + + types.add_type_info(enum, enum.name, lines) + + +def generate_enums(enums: List[model.Enum], types: TypeData) -> None: + for enum in enums: + generate_enum(enum, types) diff --git a/generator/plugins/rust/rust_file_header.py b/generator/plugins/rust/rust_file_header.py new file mode 100644 index 0000000..4928260 --- /dev/null +++ b/generator/plugins/rust/rust_file_header.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import List + + +def license_header() -> List[str]: + return [ + "Copyright (c) Microsoft Corporation. All rights reserved.", + "Licensed under the MIT License.", + ] + + +def package_description() -> List[str]: + return ["Language Server Protocol types for Rust generated from LSP specification."] diff --git a/generator/plugins/rust/rust_lang_utils.py b/generator/plugins/rust/rust_lang_utils.py new file mode 100644 index 0000000..222d928 --- /dev/null +++ b/generator/plugins/rust/rust_lang_utils.py @@ -0,0 +1,73 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +import re +from typing import List + +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]))") +DEFAULT_INDENT = " " + + +def lines_to_comments(lines: List[str]) -> List[str]: + return ["// " + line for line in lines] + + +def lines_to_doc_comments(lines: List[str]) -> List[str]: + doc = [] + for line in lines: + line = BASIC_LINK_RE.sub(r"[\2][\1]", line) + line = BASIC_LINK_RE2.sub(r"[\3][`\1::\2`]", line) + line = BASIC_LINK_RE3.sub(r"[\1]", line) + line = BASIC_LINK_RE4.sub(r"[`\1::\2`]", line) + if line.startswith("///"): + doc.append(line) + else: + doc.append("/// " + line) + return doc + + +def lines_to_block_comment(lines: List[str]) -> List[str]: + return ["/*"] + lines + ["*/"] + + +def get_parts(name: str) -> List[str]: + name = name.replace("_", " ") + return PARTS_RE.sub(r"\2 \3", name).split() + + +def to_snake_case(name: str) -> str: + return "_".join([part.lower() for part in get_parts(name)]) + + +def has_upper_case(name: str) -> bool: + return any(c.isupper() for c in name) + + +def is_snake_case(name: str) -> bool: + return ( + not name.startswith("_") + and not name.endswith("_") + and ("_" in name) + and not has_upper_case(name) + ) + + +def to_upper_camel_case(name: str) -> str: + return "".join([c.capitalize() for c in get_parts(name)]) + + +def to_camel_case(name: str) -> str: + parts = get_parts(name) + if len(parts) > 1: + return parts[0] + "".join([c.capitalize() for c in parts[1:]]) + else: + return parts[0] + + +def indent_lines(lines: List[str], indent: str = DEFAULT_INDENT) -> List[str]: + return [f"{indent}{line}" for line in lines] diff --git a/generator/plugins/rust/rust_structs.py b/generator/plugins/rust/rust_structs.py new file mode 100644 index 0000000..571cd62 --- /dev/null +++ b/generator/plugins/rust/rust_structs.py @@ -0,0 +1,460 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import Dict, Iterable, List, Optional + +import generator.model as model + +from .rust_commons import ( + TypeData, + fix_lsp_method_name, + generate_extras, + generate_literal_struct_name, + generate_property, + get_extended_properties, + get_message_type_name, + get_type_name, + struct_wrapper, + type_alias_wrapper, +) +from .rust_lang_utils import get_parts, lines_to_doc_comments, to_upper_camel_case + + +def generate_type_aliases(spec: model.LSPModel, types: TypeData) -> None: + for alias in spec.typeAliases: + if not types.has_id(alias): + generate_type_alias(alias, types, spec) + + +def _get_doc(doc: Optional[str]) -> str: + if doc: + return lines_to_doc_comments(doc.splitlines(keepends=False)) + return [] + + +def _is_some_array_type(items: Iterable[model.LSP_TYPE_SPEC]) -> bool: + items_list = list(items) + assert len(items_list) == 2 + item1, item2 = items_list + + if item1.kind == "array" and item2.kind == "reference": + return item1.element.kind == "reference" and item1.element.name == item2.name + + if item2.kind == "array" and item1.kind == "reference": + return item2.element.kind == "reference" and item2.element.name == item1.name + return False + + +def _get_some_array_code( + items: Iterable[model.LSP_TYPE_SPEC], + types: Dict[str, List[str]], + spec: model.LSPModel, +) -> List[str]: + assert _is_some_array_type(items) + items_list = list(items) + item1 = items_list[0] + item2 = items_list[1] + + if item1.kind == "array" and item2.kind == "reference": + return [ + f" One({get_type_name(item2, types, spec)}),", + f" Many({get_type_name(item1, types, spec)}),", + ] + + if item2.kind == "array" and item1.kind == "reference": + return [ + f" One({get_type_name(item1, types, spec)}),", + f" Many({get_type_name(item2, types, spec)}),", + ] + return [] + + +def _get_common_name(items: Iterable[model.LSP_TYPE_SPEC], kind: str) -> List[str]: + names = [get_parts(item.name) for item in list(items) if item.kind == kind] + if len(names) < 2: + return [] + + smallest = min(names, key=len) + common = [] + for i in range(len(smallest)): + if all(name[i] == smallest[i] for name in names): + common.append(smallest[i]) + return common + + +def _is_all_reference_similar_type(alias: model.TypeAlias) -> bool: + items_list = list(alias.type.items) + return all(item.kind in ["reference", "base", "literal"] for item in items_list) + + +def _get_all_reference_similar_code( + alias: model.TypeAlias, + types: TypeData, + spec: model.LSPModel, +) -> List[str]: + items = alias.type.items + assert _is_all_reference_similar_type(alias) + + # Ensure all literal types have a name + for item in list(items): + if item.kind == "literal": + get_type_name(item, types, spec, None, alias.name) + + common_name = [ + i.lower() + for i in ( + _get_common_name(items, "reference") + + _get_common_name(items, "literal") + + ["struct"] + ) + ] + + lines = [] + value = 0 + field_names = [] + for item in list(items): + if item.kind == "base" and item.name == "null": + lines += ["None,"] + field_names += ["None"] + elif item.kind == "base": + name = _base_to_field_name(item.name) + lines += [f"{name}({get_type_name(item, types, spec)}),"] + field_names += [name] + elif item.kind == "reference": + name = [ + part for part in get_parts(item.name) if part.lower() not in common_name + ] + if len(name) == 0: + name = [f"Value{value}"] + value += 1 + common_name += [n.lower() for n in name] + name = to_upper_camel_case("".join(name)) + field_names += [name] + lines += [f"{name}({get_type_name(item, types, spec)}),"] + elif item.kind == "literal": + name = [ + part for part in get_parts(item.name) if part.lower() not in common_name + ] + optional_props = [p for p in item.value.properties if p.optional] + required_props = [p for p in item.value.properties if not p.optional] + + # Try picking a name using required props first and then optional props + if len(name) == 0: + for p in required_props + optional_props: + name = [ + part + for part in get_parts(p.name) + if part.lower() not in common_name + ] + if len(name) != 0: + break + + # If we still don't have a name, then try picking a name using required props + # and then optional props without checking for common name list. But check + # that the name is not already used. + if len(name) == 0: + for p in required_props + optional_props: + if to_upper_camel_case(p.name) not in field_names: + name = get_parts(p.name) + break + + # If we still don't have a name, then just use a generic "Value{int}" as name + if len(name) == 0: + name = [f"Value{value}"] + value += 1 + common_name += [n.lower() for n in name] + name = to_upper_camel_case("".join(name)) + field_names += [name] + lines += [f"{name}({item.name}),"] + else: + raise ValueError(f"Unknown type {item}") + return lines + + +def _base_to_field_name(base_name: str) -> str: + if base_name == "boolean": + return "Bool" + if base_name == "integer": + return "Int" + if base_name == "decimal": + return "Real" + if base_name == "string": + return "String" + if base_name == "uinteger": + return "UInt" + if base_name == "null": + return "None" + raise ValueError(f"Unknown base type {base_name}") + + +def _get_literal_field_name(literal: model.LiteralType, types: TypeData) -> str: + properties = list(literal.value.properties) + + if len(properties) == 1 and properties[0].kind == "base": + return _base_to_field_name(properties[0].name) + + if len(properties) == 1 and properties[0].kind == "reference": + return to_upper_camel_case(properties[0].name) + + return generate_literal_struct_name(literal, types) + + +def _generate_or_type_alias( + alias_def: model.TypeAlias, types: Dict[str, List[str]], spec: model.LSPModel +) -> List[str]: + inner = [] + + if len(alias_def.type.items) == 2 and _is_some_array_type(alias_def.type.items): + inner += _get_some_array_code(alias_def.type.items, types, spec) + elif _is_all_reference_similar_type(alias_def): + inner += _get_all_reference_similar_code(alias_def, types, spec) + else: + index = 0 + + for sub_type in alias_def.type.items: + if sub_type.kind == "base" and sub_type.name == "null": + inner += [f"None,"] + else: + inner += [f"ValueType{index}({get_type_name(sub_type, types, spec)}),"] + index += 1 + return type_alias_wrapper(alias_def, inner) + + +def generate_type_alias( + alias_def: model.TypeAlias, types: TypeData, spec: model.LSPModel +) -> List[str]: + doc = _get_doc(alias_def.documentation) + doc += generate_extras(alias_def) + + lines = [] + if alias_def.type.kind == "reference": + lines += doc + lines += [f"pub type {alias_def.name} = {alias_def.type.name};"] + elif alias_def.type.kind == "array": + lines += doc + lines += [ + f"pub type {alias_def.name} = {get_type_name(alias_def.type, types, spec)};" + ] + elif alias_def.type.kind == "or": + lines += _generate_or_type_alias(alias_def, types, spec) + elif alias_def.type.kind == "and": + raise ValueError("And type not supported") + elif alias_def.type.kind == "literal": + lines += doc + lines += [ + f"pub type {alias_def.name} = {get_type_name(alias_def.type, types, spec)};" + ] + elif alias_def.type.kind == "base": + lines += doc + lines += [ + f"pub type {alias_def.name} = {get_type_name(alias_def.type, types, spec)};" + ] + else: + pass + + types.add_type_info(alias_def, alias_def.name, lines) + + +def generate_structures(spec: model.LSPModel, types: TypeData) -> Dict[str, List[str]]: + for struct in spec.structures: + if not types.has_id(struct): + generate_struct(struct, types, spec) + return types + + +def generate_struct( + struct_def: model.Structure, types: TypeData, spec: model.LSPModel +) -> None: + inner = [] + for prop_def in get_extended_properties(struct_def, spec): + inner += generate_property(prop_def, types, spec) + + lines = struct_wrapper(struct_def, inner) + types.add_type_info(struct_def, struct_def.name, lines) + + +def generate_notifications( + spec: model.LSPModel, types: TypeData +) -> Dict[str, List[str]]: + for notification in spec.notifications: + if not types.has_id(notification): + generate_notification(notification, types, spec) + return types + + +def required_rpc_properties(name: Optional[str] = None) -> List[model.Property]: + props = [ + model.Property( + name="jsonrpc", + type=model.BaseType(kind="base", name="string"), + optional=False, + documentation="The version of the JSON RPC protocol.", + ), + ] + if name: + props += [ + model.Property( + name="method", + type=model.ReferenceType(kind="reference", name=name), + optional=False, + documentation="The method to be invoked.", + ), + ] + return props + + +def generate_notification( + notification_def: model.Notification, types: TypeData, spec: model.LSPModel +) -> None: + properties = required_rpc_properties("LSPNotificationMethods") + if notification_def.params: + properties += [ + model.Property( + name="params", + type=notification_def.params, + ) + ] + + inner = [] + for prop_def in properties: + inner += generate_property(prop_def, types, spec) + + lines = struct_wrapper(notification_def, inner) + types.add_type_info( + notification_def, get_message_type_name(notification_def), lines + ) + + +def generate_required_request_types( + spec: model.LSPModel, types: TypeData +) -> Dict[str, List[str]]: + lsp_id = model.TypeAlias( + name="LSPId", + documentation="An identifier to denote a specific request.", + type=model.OrType( + kind="or", + items=[ + model.BaseType(kind="base", name="integer"), + model.BaseType(kind="base", name="string"), + ], + ), + ) + generate_type_alias(lsp_id, types, spec) + + lsp_id_optional = model.TypeAlias( + name="LSPIdOptional", + documentation="An identifier to denote a specific response.", + type=model.OrType( + kind="or", + items=[ + model.BaseType(kind="base", name="integer"), + model.BaseType(kind="base", name="string"), + model.BaseType(kind="base", name="null"), + ], + ), + ) + generate_type_alias(lsp_id_optional, types, spec) + + +def generate_requests(spec: model.LSPModel, types: TypeData) -> Dict[str, List[str]]: + generate_required_request_types(spec, types) + for request in spec.requests: + if not types.has_id(request): + generate_request(request, types, spec) + generate_response(request, types, spec) + generate_partial_result(request, types, spec) + generate_registration_options(request, types, spec) + return types + + +def generate_request( + request_def: model.Request, types: TypeData, spec: model.LSPModel +) -> None: + properties = required_rpc_properties("LSPRequestMethods") + + properties += [ + model.Property( + name="id", + type=model.ReferenceType(kind="reference", name="LSPId"), + optional=False, + documentation="The request id.", + ) + ] + if request_def.params: + properties += [ + model.Property( + name="params", + type=request_def.params, + ) + ] + + inner = [] + for prop_def in properties: + inner += generate_property(prop_def, types, spec) + + lines = struct_wrapper(request_def, inner) + types.add_type_info(request_def, get_message_type_name(request_def), lines) + + +def generate_response( + request_def: model.Request, types: TypeData, spec: model.LSPModel +) -> None: + properties = required_rpc_properties("LSPRequestMethods") + properties += [ + model.Property( + name="id", + type=model.ReferenceType(kind="reference", name="LSPIdOptional"), + optional=False, + documentation="The request id.", + ) + ] + if request_def.result: + if request_def.result.kind == "base" and request_def.result.name == "null": + properties += [ + model.Property( + name="result", + type=model.ReferenceType(kind="reference", name="LSPNull"), + ) + ] + else: + properties += [ + model.Property( + name="result", + type=request_def.result, + ) + ] + name = fix_lsp_method_name(request_def.method) + response_def = model.Structure( + name=f"{name}Response", + documentation=f"Response to the [{name}Request].", + properties=properties, + since=request_def.since, + deprecated=request_def.deprecated, + ) + + inner = [] + for prop_def in properties: + inner += generate_property(prop_def, types, spec) + + lines = struct_wrapper(response_def, inner) + types.add_type_info(response_def, response_def.name, lines) + + +def generate_partial_result( + request_def: model.Request, types: TypeData, spec: model.LSPModel +) -> None: + if not request_def.partialResult: + return + + if request_def.partialResult.kind not in ["and", "or"]: + return + + +def generate_registration_options( + request_def: model.Request, types: TypeData, spec: model.LSPModel +) -> None: + if not request_def.registrationOptions: + return + + if request_def.registrationOptions.kind not in ["and", "or"]: + return diff --git a/generator/plugins/rust/rust_utils.py b/generator/plugins/rust/rust_utils.py new file mode 100644 index 0000000..d817070 --- /dev/null +++ b/generator/plugins/rust/rust_utils.py @@ -0,0 +1,68 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pathlib +from typing import List + +from generator import model + +from .rust_commons import TypeData, generate_commons +from .rust_enum import generate_enums +from .rust_file_header import license_header +from .rust_lang_utils import lines_to_comments +from .rust_structs import ( + generate_notifications, + generate_requests, + generate_structures, + generate_type_aliases, +) + +PACKAGE_DIR_NAME = "lsprotocol" + + +def generate_from_spec(spec: model.LSPModel, output_dir: str) -> None: + code = generate_package_code(spec) + + output_path = pathlib.Path(output_dir, PACKAGE_DIR_NAME) + if not output_path.exists(): + output_path.mkdir(parents=True, exist_ok=True) + (output_path / "src").mkdir(parents=True, exist_ok=True) + + for file_name in code: + (output_path / file_name).write_text(code[file_name], encoding="utf-8") + + +def generate_package_code(spec: model.LSPModel) -> List[str]: + return { + "src/lib.rs": generate_lib_rs(spec), + } + + +def generate_lib_rs(spec: model.LSPModel) -> List[str]: + lines = lines_to_comments(license_header()) + lines += [ + "", + "// ****** THIS IS A GENERATED FILE, DO NOT EDIT. ******", + "// Steps to generate:", + "// 1. Checkout https://github.com/microsoft/lsprotocol", + "// 2. Install nox: `python -m pip install nox`", + "// 3. Run command: `python -m nox --session build_lsp`", + "", + ] + lines += [ + "use serde::{Serialize, Deserialize};", + "use std::collections::HashMap;", + "use rust_decimal::Decimal;" "", + ] + + type_data = TypeData() + generate_commons(spec, type_data) + generate_enums(spec.enumerations, type_data) + + generate_type_aliases(spec, type_data) + generate_structures(spec, type_data) + generate_notifications(spec, type_data) + generate_requests(spec, type_data) + + lines += type_data.get_lines() + return "\n".join(lines) |