From a25c8ebd727afc2f6e726245ea5f3f9f66b8b476 Mon Sep 17 00:00:00 2001 From: xXenvy Date: Fri, 31 May 2024 16:13:23 +0200 Subject: [PATCH 1/7] feat: add access generator example --- examples/access_generator/input/car.h | 22 +++++ examples/access_generator/input/human.h | 33 +++++++ examples/access_generator/main.py | 115 ++++++++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 examples/access_generator/input/car.h create mode 100644 examples/access_generator/input/human.h create mode 100644 examples/access_generator/main.py diff --git a/examples/access_generator/input/car.h b/examples/access_generator/input/car.h new file mode 100644 index 0000000..1b19c59 --- /dev/null +++ b/examples/access_generator/input/car.h @@ -0,0 +1,22 @@ +#ifndef CAR_H +#define CAR_H + +#include + +class Car { +private: + std::string brand; + float speed; + + // Ignore maybe_unused and nodiscard attributes. + // devana: ignore-attributes + bool working; +public: + [[maybe_unused]] + void speedUp() { + std::cout << "Speeeeed!" << std::endl; + setSpeed(speed + 10.1); // The setSpeed method will be generated. + } +}; + +#endif //CAR_H \ No newline at end of file diff --git a/examples/access_generator/input/human.h b/examples/access_generator/input/human.h new file mode 100644 index 0000000..4922b21 --- /dev/null +++ b/examples/access_generator/input/human.h @@ -0,0 +1,33 @@ +#ifndef HUMAN_H +#define HUMAN_H + +#include + +class Human { +private: + // Ignore maybe_unused and nodiscard attributes. + // devana: ignore-attributes + std::string name; + + std::string lastName; + + // Change the name of the methods. + // devana: custom-name=Age + int _age; + + // To ignore this field completely. + // devana: ignore-field + double height; + + double weight; +public: + Human(const std::string &name, const std::string &lastName, int age, double height, double weight): + name(name), lastName(lastName), _age(age), height(height), weight(weight) {} + + [[maybe_unused]] + void say() const { + std::cout << "Hello! My name is " << getName() << std::endl; // The getName method will be generated. + } +}; + +#endif //HUMAN_H \ No newline at end of file diff --git a/examples/access_generator/main.py b/examples/access_generator/main.py new file mode 100644 index 0000000..fe3932c --- /dev/null +++ b/examples/access_generator/main.py @@ -0,0 +1,115 @@ +import os +from typing import Iterable, Tuple, Optional + +from devana.code_generation.printers.default.defaultprinter import create_default_printer, CodePrinter +from devana.syntax_abstraction.attribute import Attribute, AttributeDeclaration +from devana.syntax_abstraction.classinfo import MethodInfo, Comment, FunctionInfo, AccessSpecifier, ClassInfo, FieldInfo +from devana.syntax_abstraction.functioninfo import BasicType, FunctionModification +from devana.syntax_abstraction.typeexpression import TypeModification +from devana.syntax_abstraction.organizers import * + + +def get_devana_comments(comment: Comment) -> Iterable[str]: + # Function to get all devana comments properly formatted. + for comment_source in comment.text: + if (formatted_comment := comment_source.lstrip()).startswith("devana:"): + yield formatted_comment + + +def get_field_name(field: FieldInfo) -> str: + # Function to retrieve the field name. + name: str = field.name.capitalize() + + if field.associated_comment: + for text in get_devana_comments(field.associated_comment): + if text.startswith("devana: custom-name="): + name = text.split("=")[1] + break + + return name + + +def create_field_methods(field: FieldInfo) -> Tuple[MethodInfo, MethodInfo]: + # Function to generate setter and getter for field. + def should_ignore_attributes(comment: Optional[Comment]) -> bool: + return "devana: ignore-attributes" in get_devana_comments(comment) if comment else False + + field_name: str = get_field_name(field) + maybe_unused_attribute: Attribute = Attribute("maybe_unused") + nodiscard_attribute: Attribute = Attribute("nodiscard") + + field_setter: MethodInfo = MethodInfo() + field_setter.body = f"{field.name} = new{field.name.capitalize()};" + field_setter.access_specifier = AccessSpecifier.PUBLIC + field_setter.name = f"set{field_name}" + field_setter.return_type = BasicType.VOID + + setter_arg: FunctionInfo.Argument = FunctionInfo.Argument() + setter_arg.name = f"new{field.name.capitalize()}" + setter_arg.type.details = field.type + if field.type.name == "string": + setter_arg.type.modification |= TypeModification.CONST | TypeModification.REFERENCE + + field_setter.arguments.append(setter_arg) + + field_getter: MethodInfo = MethodInfo() + field_getter.body = f"return {field.name};" + field_getter.access_specifier = AccessSpecifier.PUBLIC + field_getter.name = f"get{field_name}" + field_getter.return_type = field.type + field_getter.modification |= FunctionModification.CONST + + if not should_ignore_attributes(field.associated_comment): + field_getter.attributes.append(AttributeDeclaration([nodiscard_attribute, maybe_unused_attribute])) + field_setter.attributes.append(AttributeDeclaration([maybe_unused_attribute])) + + return field_setter, field_getter + + +def create_class_methods(class_info: ClassInfo) -> None: + # Function to generate methods for all class fields. + def should_ignore_field(comment: Optional[Comment]) -> bool: + return "devana: ignore-field" in get_devana_comments(comment) if comment else False + + for private_field in filter(lambda field: isinstance(field, FieldInfo), class_info.private): + if should_ignore_field(private_field.associated_comment): + continue + + class_info.content.extend(create_field_methods(field=private_field)) + + return None + + +def load_files_content() -> Iterable[Tuple[str, Iterable[ClassInfo]]]: + module_filter: ModuleFilter = ModuleFilter(allowed_filter=[".h"]) # Accept only header files. + source: SourceModule = SourceModule(name="HEADERS", root_path="./input", module_filter=module_filter) + for file in source.files: + yield file.name, filter(lambda element: isinstance(element, ClassInfo), file.content) + + +def main() -> None: + if not os.path.exists("./output"): + os.makedirs(os.path.dirname("./output/")) + + printer: CodePrinter = create_default_printer() + + iostream_include = IncludeInfo() + iostream_include.value = "iostream" + iostream_include.is_standard = True # to write this as instead of "iostream" + + for file_name, file_content in load_files_content(): + header_file: SourceFile = SourceFile() + header_file.type = SourceFileType.HEADER + header_file.header_guard = file_name.upper().replace(".", "_") + header_file.includes.append(iostream_include) + + for class_info in file_content: + create_class_methods(class_info) + header_file.content.append(class_info) + + with open(f"./output/{file_name}", "w+") as f: + f.write(printer.print(header_file)) + + +if __name__ == "__main__": + main() From cea6bb9f3509b6eef7c5b0b6b5d578ca720b38b8 Mon Sep 17 00:00:00 2001 From: xXenvy Date: Fri, 31 May 2024 16:14:31 +0200 Subject: [PATCH 2/7] fix(example): add missing comma to enums.h --- examples/meta_enum/input/enums.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/meta_enum/input/enums.h b/examples/meta_enum/input/enums.h index f45d28d..2c2cb97 100644 --- a/examples/meta_enum/input/enums.h +++ b/examples/meta_enum/input/enums.h @@ -12,7 +12,7 @@ enum class AnimalPetKind enum class AnimalPetState { - OK = 0x0 + OK = 0x0, HUNGRY = 0x2, SICK = 0x4, ANGRY = 0x6 From ab7704040dc5222e39fd51e42badad41dd6bf6c3 Mon Sep 17 00:00:00 2001 From: xXenvy Date: Sun, 2 Jun 2024 21:46:24 +0200 Subject: [PATCH 3/7] docs: add missing demo --- doc_src/demos/demo_accessor_generator.rst | 6 ++++++ doc_src/how_to_start.rst | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 doc_src/demos/demo_accessor_generator.rst diff --git a/doc_src/demos/demo_accessor_generator.rst b/doc_src/demos/demo_accessor_generator.rst new file mode 100644 index 0000000..7e21f90 --- /dev/null +++ b/doc_src/demos/demo_accessor_generator.rst @@ -0,0 +1,6 @@ +Generate setters and getters for private fields +=============================================== + +Please find demo code in our `GitHub `_. + +This demo program automatically generates setter and getter methods for private fields within C++ classes. The process specifically targets header files. diff --git a/doc_src/how_to_start.rst b/doc_src/how_to_start.rst index 010b644..8c0238b 100644 --- a/doc_src/how_to_start.rst +++ b/doc_src/how_to_start.rst @@ -84,4 +84,5 @@ Examples :maxdepth: 1 demos/demo_include_map - demos/demo_meta_info_enum \ No newline at end of file + demos/demo_meta_info_enum + demos/demo_accessor_generator \ No newline at end of file From fd546e7f326776a713892594333dfe0352fe76ca Mon Sep 17 00:00:00 2001 From: xXenvy Date: Sun, 2 Jun 2024 21:47:50 +0200 Subject: [PATCH 4/7] refactor: remove unnecessary return + specify encoding --- examples/{access_generator => accessor_generator}/main.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) rename examples/{access_generator => accessor_generator}/main.py (98%) diff --git a/examples/access_generator/main.py b/examples/accessor_generator/main.py similarity index 98% rename from examples/access_generator/main.py rename to examples/accessor_generator/main.py index fe3932c..5b52ce4 100644 --- a/examples/access_generator/main.py +++ b/examples/accessor_generator/main.py @@ -77,8 +77,6 @@ def should_ignore_field(comment: Optional[Comment]) -> bool: class_info.content.extend(create_field_methods(field=private_field)) - return None - def load_files_content() -> Iterable[Tuple[str, Iterable[ClassInfo]]]: module_filter: ModuleFilter = ModuleFilter(allowed_filter=[".h"]) # Accept only header files. @@ -107,7 +105,7 @@ def main() -> None: create_class_methods(class_info) header_file.content.append(class_info) - with open(f"./output/{file_name}", "w+") as f: + with open(f"./output/{file_name}", "w+", encoding="utf-8") as f: f.write(printer.print(header_file)) From 653614c208fde8c31bc8b175839b9cf99acfffee Mon Sep 17 00:00:00 2001 From: xXenvy Date: Sun, 2 Jun 2024 21:55:15 +0200 Subject: [PATCH 5/7] Oops i forgot to commit movied files --- examples/{access_generator => accessor_generator}/input/car.h | 0 examples/{access_generator => accessor_generator}/input/human.h | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename examples/{access_generator => accessor_generator}/input/car.h (100%) rename examples/{access_generator => accessor_generator}/input/human.h (100%) diff --git a/examples/access_generator/input/car.h b/examples/accessor_generator/input/car.h similarity index 100% rename from examples/access_generator/input/car.h rename to examples/accessor_generator/input/car.h diff --git a/examples/access_generator/input/human.h b/examples/accessor_generator/input/human.h similarity index 100% rename from examples/access_generator/input/human.h rename to examples/accessor_generator/input/human.h From 6cb2adf143e551c5d92e1a76e853e6d2389be2c3 Mon Sep 17 00:00:00 2001 From: xXenvy Date: Sun, 2 Jun 2024 23:28:58 +0200 Subject: [PATCH 6/7] refactor: resolve requested changes --- ...rator.rst => demo_accessors_generator.rst} | 0 doc_src/how_to_start.rst | 2 +- .../input/car.h | 2 +- .../input/human.h | 4 +-- .../main.py | 35 +++++++++++++------ 5 files changed, 27 insertions(+), 16 deletions(-) rename doc_src/demos/{demo_accessor_generator.rst => demo_accessors_generator.rst} (100%) rename examples/{accessor_generator => accessors_generator}/input/car.h (87%) rename examples/{accessor_generator => accessors_generator}/input/human.h (83%) rename examples/{accessor_generator => accessors_generator}/main.py (78%) diff --git a/doc_src/demos/demo_accessor_generator.rst b/doc_src/demos/demo_accessors_generator.rst similarity index 100% rename from doc_src/demos/demo_accessor_generator.rst rename to doc_src/demos/demo_accessors_generator.rst diff --git a/doc_src/how_to_start.rst b/doc_src/how_to_start.rst index 8c0238b..c0f736b 100644 --- a/doc_src/how_to_start.rst +++ b/doc_src/how_to_start.rst @@ -85,4 +85,4 @@ Examples demos/demo_include_map demos/demo_meta_info_enum - demos/demo_accessor_generator \ No newline at end of file + demos/demo_accessors_generator \ No newline at end of file diff --git a/examples/accessor_generator/input/car.h b/examples/accessors_generator/input/car.h similarity index 87% rename from examples/accessor_generator/input/car.h rename to examples/accessors_generator/input/car.h index 1b19c59..1bc6e4e 100644 --- a/examples/accessor_generator/input/car.h +++ b/examples/accessors_generator/input/car.h @@ -2,13 +2,13 @@ #define CAR_H #include +#include class Car { private: std::string brand; float speed; - // Ignore maybe_unused and nodiscard attributes. // devana: ignore-attributes bool working; public: diff --git a/examples/accessor_generator/input/human.h b/examples/accessors_generator/input/human.h similarity index 83% rename from examples/accessor_generator/input/human.h rename to examples/accessors_generator/input/human.h index 4922b21..8aa99a1 100644 --- a/examples/accessor_generator/input/human.h +++ b/examples/accessors_generator/input/human.h @@ -2,20 +2,18 @@ #define HUMAN_H #include +#include class Human { private: - // Ignore maybe_unused and nodiscard attributes. // devana: ignore-attributes std::string name; std::string lastName; - // Change the name of the methods. // devana: custom-name=Age int _age; - // To ignore this field completely. // devana: ignore-field double height; diff --git a/examples/accessor_generator/main.py b/examples/accessors_generator/main.py similarity index 78% rename from examples/accessor_generator/main.py rename to examples/accessors_generator/main.py index 5b52ce4..4f6311b 100644 --- a/examples/accessor_generator/main.py +++ b/examples/accessors_generator/main.py @@ -1,12 +1,12 @@ import os from typing import Iterable, Tuple, Optional +from devana.syntax_abstraction.classinfo import MethodInfo, Comment, FunctionInfo, AccessSpecifier, ClassInfo, FieldInfo, ConstructorInfo +from devana.syntax_abstraction.organizers import ModuleFilter, SourceModule, SourceFile, SourceFileType, IncludeInfo from devana.code_generation.printers.default.defaultprinter import create_default_printer, CodePrinter -from devana.syntax_abstraction.attribute import Attribute, AttributeDeclaration -from devana.syntax_abstraction.classinfo import MethodInfo, Comment, FunctionInfo, AccessSpecifier, ClassInfo, FieldInfo from devana.syntax_abstraction.functioninfo import BasicType, FunctionModification +from devana.syntax_abstraction.attribute import Attribute, AttributeDeclaration from devana.syntax_abstraction.typeexpression import TypeModification -from devana.syntax_abstraction.organizers import * def get_devana_comments(comment: Comment) -> Iterable[str]: @@ -60,17 +60,26 @@ def should_ignore_attributes(comment: Optional[Comment]) -> bool: field_getter.modification |= FunctionModification.CONST if not should_ignore_attributes(field.associated_comment): - field_getter.attributes.append(AttributeDeclaration([nodiscard_attribute, maybe_unused_attribute])) - field_setter.attributes.append(AttributeDeclaration([maybe_unused_attribute])) + field_getter.attributes.append( + AttributeDeclaration([nodiscard_attribute, maybe_unused_attribute]) + ) + field_setter.attributes.append( + AttributeDeclaration([maybe_unused_attribute]) + ) return field_setter, field_getter -def create_class_methods(class_info: ClassInfo) -> None: +def create_class_methods(class_info: ClassInfo): # Function to generate methods for all class fields. def should_ignore_field(comment: Optional[Comment]) -> bool: return "devana: ignore-field" in get_devana_comments(comment) if comment else False + for constructor in filter(lambda constr: isinstance(constr, ConstructorInfo), class_info.constructors): + if constructor.body == "{}": + # We need to define body (as empty in this case) to say devana: it is function definition. + constructor.body = "" + for private_field in filter(lambda field: isinstance(field, FieldInfo), class_info.private): if should_ignore_field(private_field.associated_comment): continue @@ -80,12 +89,12 @@ def should_ignore_field(comment: Optional[Comment]) -> bool: def load_files_content() -> Iterable[Tuple[str, Iterable[ClassInfo]]]: module_filter: ModuleFilter = ModuleFilter(allowed_filter=[".h"]) # Accept only header files. - source: SourceModule = SourceModule(name="HEADERS", root_path="./input", module_filter=module_filter) + source: SourceModule = SourceModule("HEADERS", "./input", module_filter=module_filter) for file in source.files: yield file.name, filter(lambda element: isinstance(element, ClassInfo), file.content) -def main() -> None: +def main(): if not os.path.exists("./output"): os.makedirs(os.path.dirname("./output/")) @@ -95,18 +104,22 @@ def main() -> None: iostream_include.value = "iostream" iostream_include.is_standard = True # to write this as instead of "iostream" + string_include = IncludeInfo() + string_include.value = "string" + string_include.is_standard = True # to write this as instead of "string" + for file_name, file_content in load_files_content(): header_file: SourceFile = SourceFile() header_file.type = SourceFileType.HEADER header_file.header_guard = file_name.upper().replace(".", "_") - header_file.includes.append(iostream_include) + header_file.includes.extend([string_include, iostream_include]) for class_info in file_content: create_class_methods(class_info) header_file.content.append(class_info) - with open(f"./output/{file_name}", "w+", encoding="utf-8") as f: - f.write(printer.print(header_file)) + with open(f"./output/{file_name}", "w+", encoding="utf-8") as file: + file.write(printer.print(header_file)) if __name__ == "__main__": From 56041239a4705be7a4d1acb8fc9ebb753e1a54f8 Mon Sep 17 00:00:00 2001 From: xXenvy Date: Fri, 7 Jun 2024 18:12:27 +0200 Subject: [PATCH 7/7] refactor: apply suggestions from review --- examples/accessors_generator/input/car.h | 5 +- examples/accessors_generator/input/human.h | 3 +- examples/accessors_generator/main.py | 53 +++++++++++++++------- 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/examples/accessors_generator/input/car.h b/examples/accessors_generator/input/car.h index 1bc6e4e..4446e90 100644 --- a/examples/accessors_generator/input/car.h +++ b/examples/accessors_generator/input/car.h @@ -12,10 +12,9 @@ class Car { // devana: ignore-attributes bool working; public: - [[maybe_unused]] void speedUp() { - std::cout << "Speeeeed!" << std::endl; - setSpeed(speed + 10.1); // The setSpeed method will be generated. + std::cout << "Speeeeed!" << std::endl; + setSpeed(speed + 10.1); // The setSpeed method will be generated. } }; diff --git a/examples/accessors_generator/input/human.h b/examples/accessors_generator/input/human.h index 8aa99a1..a0cd524 100644 --- a/examples/accessors_generator/input/human.h +++ b/examples/accessors_generator/input/human.h @@ -22,9 +22,8 @@ class Human { Human(const std::string &name, const std::string &lastName, int age, double height, double weight): name(name), lastName(lastName), _age(age), height(height), weight(weight) {} - [[maybe_unused]] void say() const { - std::cout << "Hello! My name is " << getName() << std::endl; // The getName method will be generated. + std::cout << "Hello! My name is " << getName() << std::endl; // The getName method will be generated. } }; diff --git a/examples/accessors_generator/main.py b/examples/accessors_generator/main.py index 4f6311b..4376804 100644 --- a/examples/accessors_generator/main.py +++ b/examples/accessors_generator/main.py @@ -1,14 +1,27 @@ -import os -from typing import Iterable, Tuple, Optional +from typing import Iterable, Tuple, Optional, List +from dataclasses import dataclass +from os import path, makedirs -from devana.syntax_abstraction.classinfo import MethodInfo, Comment, FunctionInfo, AccessSpecifier, ClassInfo, FieldInfo, ConstructorInfo -from devana.syntax_abstraction.organizers import ModuleFilter, SourceModule, SourceFile, SourceFileType, IncludeInfo from devana.code_generation.printers.default.defaultprinter import create_default_printer, CodePrinter +from devana.syntax_abstraction.classinfo import MethodInfo, Comment, FunctionInfo, AccessSpecifier, ClassInfo, FieldInfo +from devana.syntax_abstraction.organizers import ModuleFilter, SourceModule, SourceFile, SourceFileType, IncludeInfo +from devana.syntax_abstraction.syntax import ISyntaxElement + from devana.syntax_abstraction.functioninfo import BasicType, FunctionModification from devana.syntax_abstraction.attribute import Attribute, AttributeDeclaration from devana.syntax_abstraction.typeexpression import TypeModification +@dataclass +class HeaderFileData: + name: str + content: List[ISyntaxElement] + + @property + def class_infos(self) -> Iterable[ClassInfo]: + return filter(lambda element: isinstance(element, ClassInfo), self.content) + + def get_devana_comments(comment: Comment) -> Iterable[str]: # Function to get all devana comments properly formatted. for comment_source in comment.text: @@ -38,13 +51,14 @@ def should_ignore_attributes(comment: Optional[Comment]) -> bool: maybe_unused_attribute: Attribute = Attribute("maybe_unused") nodiscard_attribute: Attribute = Attribute("nodiscard") - field_setter: MethodInfo = MethodInfo() + field_setter = MethodInfo() field_setter.body = f"{field.name} = new{field.name.capitalize()};" field_setter.access_specifier = AccessSpecifier.PUBLIC + field_setter.name = f"set{field_name}" field_setter.return_type = BasicType.VOID - setter_arg: FunctionInfo.Argument = FunctionInfo.Argument() + setter_arg = FunctionInfo.Argument() setter_arg.name = f"new{field.name.capitalize()}" setter_arg.type.details = field.type if field.type.name == "string": @@ -52,9 +66,10 @@ def should_ignore_attributes(comment: Optional[Comment]) -> bool: field_setter.arguments.append(setter_arg) - field_getter: MethodInfo = MethodInfo() + field_getter = MethodInfo() field_getter.body = f"return {field.name};" field_getter.access_specifier = AccessSpecifier.PUBLIC + field_getter.name = f"get{field_name}" field_getter.return_type = field.type field_getter.modification |= FunctionModification.CONST @@ -75,7 +90,7 @@ def create_class_methods(class_info: ClassInfo): def should_ignore_field(comment: Optional[Comment]) -> bool: return "devana: ignore-field" in get_devana_comments(comment) if comment else False - for constructor in filter(lambda constr: isinstance(constr, ConstructorInfo), class_info.constructors): + for constructor in class_info.constructors: if constructor.body == "{}": # We need to define body (as empty in this case) to say devana: it is function definition. constructor.body = "" @@ -87,16 +102,16 @@ def should_ignore_field(comment: Optional[Comment]) -> bool: class_info.content.extend(create_field_methods(field=private_field)) -def load_files_content() -> Iterable[Tuple[str, Iterable[ClassInfo]]]: - module_filter: ModuleFilter = ModuleFilter(allowed_filter=[".h"]) # Accept only header files. - source: SourceModule = SourceModule("HEADERS", "./input", module_filter=module_filter) +def load_files_content() -> Iterable[HeaderFileData]: + module_filter = ModuleFilter(allowed_filter=[".h"]) # Accept only header files. + source = SourceModule("HEADERS", "./input", module_filter=module_filter) for file in source.files: - yield file.name, filter(lambda element: isinstance(element, ClassInfo), file.content) + yield HeaderFileData(file.name, file.content) def main(): - if not os.path.exists("./output"): - os.makedirs(os.path.dirname("./output/")) + if not path.exists("./output"): + makedirs(path.dirname("./output/")) printer: CodePrinter = create_default_printer() @@ -108,13 +123,17 @@ def main(): string_include.value = "string" string_include.is_standard = True # to write this as instead of "string" - for file_name, file_content in load_files_content(): - header_file: SourceFile = SourceFile() + for file_data in load_files_content(): + file_name: str = file_data.name + + header_file = SourceFile() header_file.type = SourceFileType.HEADER + + # Convert guard from 'EXAMPLE.H' to 'EXAMPLE_H' header_file.header_guard = file_name.upper().replace(".", "_") header_file.includes.extend([string_include, iostream_include]) - for class_info in file_content: + for class_info in file_data.class_infos: create_class_methods(class_info) header_file.content.append(class_info)