From f5a3a417bd75b1873d2aa4fa58f09bb6c06094f3 Mon Sep 17 00:00:00 2001 From: TimTheHero <117226730+TimTheHero@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:10:40 +0100 Subject: [PATCH 01/11] feat: Update Nodefeatures - Added Field, FIeldSignature, FieldDefinition, Read, Write, ReadWrite and Interface - Sorted according to Fig 2.2 p.10 Constanzes Batchlor Thesis. -Added Todos and Comments for NodeFeatures --- NodeFeatures.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/NodeFeatures.py b/NodeFeatures.py index 41f5a24..8505f4a 100644 --- a/NodeFeatures.py +++ b/NodeFeatures.py @@ -9,11 +9,28 @@ class NodeTypes(Enum): Each node type corresponds to a specific element in the type system, providing a way to categorize and manage these elements programmatically. """ - CALL = "TCall" - CLASS = "TClass" + # TPackage + PACKAGE = "TPackage" + # TModule MODULE = "TModule" + # TClass + CLASS = "TClass" + # TMethod METHOD = "TMethod" METHOD_SIGNATURE = "TMethodSignature" METHOD_DEFINITION = "TMethodDefinition" - PACKAGE = "TPackage" PARAMETER = "TParameter" + # TField + FIELD = "TField" # Todo implement this in AstToEcoreConverter + FIELD_SIGNATURE = "TFieldSignature" # Todo implement this in AstToEcoreConverter + FIELD_DEFINITION = "TFieldDefinition" # Todo implement this in AstToEcoreConverter + # TAccess + CALL = "TCall" + READ = "TRead" # Todo implement this in AstToEcoreConverter + WRITE = "TWrite" # Todo implement this in AstToEcoreConverter + READ_WRITE = "TReadWrite" # Todo implement this in AstToEcoreConverter + #TInterface + INTERFACE = "TInterface" + # In Python, there is no formal concept of interfaces as found in some other programming languages like Java or C#. + # However, Python supports a similar concept through the use of abstract base classes (ABCs) and duck typing. + # The return on investment probably is not sufficient to justify the implementation. From f5dbd6e03aea1feda92d38935a77590b3166875e Mon Sep 17 00:00:00 2001 From: TimTheHero <117226730+TimTheHero@users.noreply.github.com> Date: Wed, 22 Jan 2025 19:06:49 +0100 Subject: [PATCH 02/11] feat: Implement ftfield, tfieldsignature and tfielddefinition - created create_field func. to create a field and sett the values according to meta model - if self.process_file fails, we now throw the exception if it is not "invalid syntax" - wrote func. get_field_from_internal_structure - added fieldcreation for visit_assign() in Ast Visitor. --- AstToEcoreConverter.py | 96 ++++++++++++++++++- .../minimal_examples/assignments/my_module.py | 7 ++ tests/test_minimal_examples.py | 82 +++++++++------- 3 files changed, 146 insertions(+), 39 deletions(-) create mode 100644 tests/minimal_examples/assignments/my_module.py diff --git a/AstToEcoreConverter.py b/AstToEcoreConverter.py index e450ac6..38bcadd 100644 --- a/AstToEcoreConverter.py +++ b/AstToEcoreConverter.py @@ -42,6 +42,7 @@ def __init__(self, resource_set: ResourceSet, repository, write_in_file, output_ # initialize internal structures self.package_list = [] # entries: [package_node, name, parent] self.module_list = [] # entries: [module_node, module_name] + self.field_list = [] # entries: [field_node, name, type, module_node] self.current_module = None self.instances = [] # entries: [instance_name, class_name] self.imports = [] # entries: [module, alias] @@ -100,6 +101,7 @@ def __init__(self, resource_set: ResourceSet, repository, write_in_file, output_ logger.warning(f'skipped: {file_path}') skipped_files += 1 continue # skip file + else: raise e # append modules and possibly missing nodes to type graph to set calls after self.append_modules() @@ -1127,7 +1129,7 @@ def create_method_signature(self, method_node, name, arguments): self.method_list.append([method_node, name, module_node]) @staticmethod - def get_calls(caller_node, called_node): + def get_calls(caller_node, called_node) -> bool: """ Checks if a call already exists between two nodes. @@ -1143,7 +1145,7 @@ def get_calls(caller_node, called_node): return True return False - def create_call(self, caller_node, called_node): + def create_call(self, caller_node, called_node) -> None: """ Creates a call between two nodes. @@ -1155,6 +1157,57 @@ def create_call(self, caller_node, called_node): call.source = caller_node call.target = called_node + def create_field(self, field_node, name, field_type=None) -> None: + """ + Creates a field for a class or module. + + Args: + field_node: The node to which the field belongs. + name (str): The name of the field. + field_type (str, optional): The type of the field. Defaults to None. + """ + field = self.create_ecore_instance(NodeTypes.FIELD) + field_signature = self.create_ecore_instance(NodeTypes.FIELD_SIGNATURE) + field_definition = self.create_ecore_instance(NodeTypes.FIELD_DEFINITION) + + field_definition.signature = field_signature + field_signature.definition = field_definition + field_signature.field = field + field.signature = field_signature + field.tName = name + + if field_type is not None: + field_signature.type = field_type + #Todo currently field_type is bugged. It sets "" instead of the correct type + + self.graph.fields.append(field) + + # for internal structure + module_node = self.get_current_module() + self.field_list.append([field_node, name, field_type, module_node]) + + field_node.contains.append(field_definition) + + def get_field_from_internal_structure(self, field_name, module=None): + """ + Retrieves a field from the internal structure by name and module. + + Args: + field_name (str): The name of the field. + module: The module to which the field belongs. + + Returns: + The field node or None if not found. + """ + for current_field in self.field_list: + if field_name == current_field[1]: + if module is None and current_field[2] is None: + return current_field[0] + if hasattr(module, 'location') and hasattr(current_field[2], 'location'): + if module.location == current_field[2].location: + return current_field[0] + return None + def write_xmi(self, resource_set, output_directory, repository): """ Writes the graph to an XMI file. @@ -1194,6 +1247,18 @@ def __init__(self, ecore_graph): self.names_in_scope: set = set() self.fields_per_class: dict = dict() + self.INTEGER_TYPE = self.ecore_graph.get_class_by_name("int") + self.FLOAT_TYPE = self.ecore_graph.get_class_by_name("float") + self.STRING_TYPE = self.ecore_graph.get_class_by_name("str") + self.BOOL_TYPE = self.ecore_graph.get_class_by_name("bool") + self.LIST_TYPE = self.ecore_graph.get_class_by_name("list") + self.TUPLE_TYPE = self.ecore_graph.get_class_by_name("tuple") + self.DICT_TYPE = self.ecore_graph.get_class_by_name("dict") + self.SET_TYPE = self.ecore_graph.get_class_by_name("set") + + def get_class_by_type(self, type_obj): + return self.ecore_graph.get_class_by_name(type_obj.__name__) + def visit_Import(self, node): """ Visits an import statement in the AST. @@ -1343,10 +1408,35 @@ def visit_Assign(self, node): if isinstance(target,ast.Attribute): if isinstance(target.value,ast.Name): if target.value.id == 'self': + field_name = target.attr + field_type = None + if isinstance(node.value,ast.Call): + field_type = node.value.func + elif isinstance(node.value,ast.Name): + field_type = node.value.id + elif isinstance(node.value,ast.Attribute): + field_type = node.value.attr + self.ecore_graph.create_field(self.current_class,field_name,field_type) if self.current_class not in self.fields_per_class: self.fields_per_class[self.current_class] = set() self.fields_per_class[self.current_class].add(target.attr) - # Todo: Use class fields in ecore model here + + # Find all module-level variables assignments: + if self.current_class is None: + for target in node.targets: + if isinstance(target, ast.Name): + field_name = target.id + field_type = None + if isinstance(node.value,ast.Constant): + field_type = self.get_class_by_type(type(node.value.value)) + elif isinstance(node.value, ast.Call): + field_type = node.value.func + elif isinstance(node.value, ast.Name): + field_type = node.value.id + elif isinstance(node.value, ast.Attribute): + field_type = node.value.attr + module_node = self.ecore_graph.get_current_module() + self.ecore_graph.create_field(module_node, field_name, field_type) if node.col_offset <= self.current_indentation: self.current_method = None diff --git a/tests/minimal_examples/assignments/my_module.py b/tests/minimal_examples/assignments/my_module.py new file mode 100644 index 0000000..ccc430c --- /dev/null +++ b/tests/minimal_examples/assignments/my_module.py @@ -0,0 +1,7 @@ +a = 1 +b = 1.1 +c = "Test" +d = True +e = [] +f = () +g = {} diff --git a/tests/test_minimal_examples.py b/tests/test_minimal_examples.py index 5559c76..0d9c72d 100644 --- a/tests/test_minimal_examples.py +++ b/tests/test_minimal_examples.py @@ -19,44 +19,54 @@ class TestMinimalExamples(unittest.TestCase): - # def test_function_overwrite(self): - # """ - # This test tests a Skript with 2 Functions with the same name. - # """ - # repo = 'minimal_examples/function_overwrite' - # check_path_exists(repo) - # resource_set = ResourceSet() - # graph = ProjectEcoreGraph(resource_set, repo, True, test_output_dir) - # ecore_graph = graph.get_graph() - # - # def test_2_functions_without_class(self): - # """ - # This test tests a Skript with 2 Functions with the same name. - # """ - # repo = 'minimal_examples/2_Function_without_class' - # check_path_exists(repo) - # resource_set = ResourceSet() - # graph = ProjectEcoreGraph(resource_set, repo, True, test_output_dir) - # ecore_graph = graph.get_graph() - # - # def test_2_functions_with_class(self): - # """ - # This test tests a Skript with 2 Functions with the same name. - # """ - # repo = 'minimal_examples/2_Functions_with_class' - # check_path_exists(repo) - # resource_set = ResourceSet() - # graph = ProjectEcoreGraph(resource_set, repo, True, test_output_dir) - # ecore_graph = graph.get_graph() - - #def test_pyinputplus(self): + def test_function_overwrite(self): + """ + This test tests a Skript with 2 Functions with the same name. + """ + repo = 'minimal_examples/function_overwrite' + check_path_exists(repo) + resource_set = ResourceSet() + graph = ProjectEcoreGraph(resource_set, repo, True, test_output_dir) + ecore_graph = graph.get_graph() + + def test_2_functions_without_class(self): + """ + This test tests a Skript with 2 Functions with the same name. + """ + repo = 'minimal_examples/2_Function_without_class' + check_path_exists(repo) + resource_set = ResourceSet() + graph = ProjectEcoreGraph(resource_set, repo, True, test_output_dir) + ecore_graph = graph.get_graph() + + def test_2_functions_with_class(self): + """ + This test tests a Skript with 2 Functions with the same name. + """ + repo = 'minimal_examples/2_Functions_with_class' + check_path_exists(repo) + resource_set = ResourceSet() + graph = ProjectEcoreGraph(resource_set, repo, True, test_output_dir) + ecore_graph = graph.get_graph() + + def test_pyinputplus(self): """ This test for pyinputplus as a test Create a dir in minimal_examples named "test_pyinputplus" use cd tests; cd minimal_examples;cd test_pyinputplus; git clone https://github.com/asweigart/pyinputplus.git """ - #repo = 'minimal_examples/test_pyinputplus/pyinputplus' - #check_path_exists(repo) - #resource_set = ResourceSet() - #graph = ProjectEcoreGraph(resource_set, repo, True, test_output_dir) - #ecore_graph = graph.get_graph() + repo = 'minimal_examples/test_pyinputplus/pyinputplus' + check_path_exists(repo) + resource_set = ResourceSet() + graph = ProjectEcoreGraph(resource_set, repo, True, test_output_dir) + ecore_graph = graph.get_graph() + + def test_assignment(self) : + """ + + """ + repo = 'minimal_examples/assignments' + check_path_exists(repo) + resource_set = ResourceSet() + graph = ProjectEcoreGraph(resource_set, repo, True, test_output_dir) + ecore_graph = graph.get_graph() From 88825cf26506802c06d0863623dee51c5415557a Mon Sep 17 00:00:00 2001 From: TimTheHero <117226730+TimTheHero@users.noreply.github.com> Date: Wed, 22 Jan 2025 19:32:44 +0100 Subject: [PATCH 03/11] fix now correct propagation --- AstToEcoreConverter.py | 44 ++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/AstToEcoreConverter.py b/AstToEcoreConverter.py index 38bcadd..aed28b6 100644 --- a/AstToEcoreConverter.py +++ b/AstToEcoreConverter.py @@ -42,7 +42,7 @@ def __init__(self, resource_set: ResourceSet, repository, write_in_file, output_ # initialize internal structures self.package_list = [] # entries: [package_node, name, parent] self.module_list = [] # entries: [module_node, module_name] - self.field_list = [] # entries: [field_node, name, type, module_node] + self.field_list = [] # entries: [field_node, name, type, module_node] self.current_module = None self.instances = [] # entries: [instance_name, class_name] self.imports = [] # entries: [module, alias] @@ -101,7 +101,8 @@ def __init__(self, resource_set: ResourceSet, repository, write_in_file, output_ logger.warning(f'skipped: {file_path}') skipped_files += 1 continue # skip file - else: raise e + else: + raise e # append modules and possibly missing nodes to type graph to set calls after self.append_modules() @@ -1176,9 +1177,7 @@ def create_field(self, field_node, name, field_type=None) -> None: field.signature = field_signature field.tName = name - if field_type is not None: - field_signature.type = field_type - #Todo currently field_type is bugged. It sets "" instead of the correct type + # field_signature.type = field_type #Todo currently bugged get str('datatype)') probably need TAbstractType self.graph.fields.append(field) @@ -1370,7 +1369,7 @@ def visit_FunctionDef(self, node): if node.name in self.names_in_scope: warning(f"Def {node.name} already in Scope") self.names_in_scope.add(node.name) - temp_scope = self.names_in_scope # save previous scope in temp for later access. + temp_scope = self.names_in_scope # save previous scope in temp for later access. self.names_in_scope = set() temp_class, temp_method = self.current_class, self.current_method self.current_method = None @@ -1378,7 +1377,6 @@ def visit_FunctionDef(self, node): self.current_method = self.ecore_graph.get_method_def_in_class( node.name, self.current_class) if self.current_method is None: - self.current_class = None self.current_method = self.ecore_graph.create_ecore_instance( NodeTypes.METHOD_DEFINITION) @@ -1391,9 +1389,9 @@ def visit_FunctionDef(self, node): self.generic_visit(node) - self.current_class,self.current_method = temp_class, temp_method # Restore current class and method + self.current_class, self.current_method = temp_class, temp_method # Restore current class and method - self.names_in_scope = temp_scope # Restore Scope from node before + self.names_in_scope = temp_scope # Restore Scope from node before def visit_Assign(self, node): """ @@ -1405,18 +1403,23 @@ def visit_Assign(self, node): # Find all field assignments in a class if self.current_class is not None: for target in node.targets: - if isinstance(target,ast.Attribute): - if isinstance(target.value,ast.Name): + if isinstance(target, ast.Attribute): + if isinstance(target.value, ast.Name): if target.value.id == 'self': field_name = target.attr field_type = None - if isinstance(node.value,ast.Call): - field_type = node.value.func - elif isinstance(node.value,ast.Name): + if isinstance(node.value, ast.Constant): + field_type = type(node.value.value).__name__ + elif isinstance(node.value, ast.Call): + if isinstance(node.value.func, ast.Name): + field_type = node.value.func.id + elif isinstance(node.value.func, ast.Attribute): + field_type = node.value.func.attr + elif isinstance(node.value, ast.Name): field_type = node.value.id - elif isinstance(node.value,ast.Attribute): + elif isinstance(node.value, ast.Attribute): field_type = node.value.attr - self.ecore_graph.create_field(self.current_class,field_name,field_type) + self.ecore_graph.create_field(self.current_class, field_name, field_type) if self.current_class not in self.fields_per_class: self.fields_per_class[self.current_class] = set() self.fields_per_class[self.current_class].add(target.attr) @@ -1427,10 +1430,13 @@ def visit_Assign(self, node): if isinstance(target, ast.Name): field_name = target.id field_type = None - if isinstance(node.value,ast.Constant): - field_type = self.get_class_by_type(type(node.value.value)) + if isinstance(node.value, ast.Constant): + field_type = type(node.value.value).__name__ elif isinstance(node.value, ast.Call): - field_type = node.value.func + if isinstance(node.value.func, ast.Name): + field_type = node.value.func.id + elif isinstance(node.value.func, ast.Attribute): + field_type = node.value.func.attr elif isinstance(node.value, ast.Name): field_type = node.value.id elif isinstance(node.value, ast.Attribute): From 8d4cfe774f34d9fc6ffe947739eccc6c23736a4f Mon Sep 17 00:00:00 2001 From: TimTheHero <117226730+TimTheHero@users.noreply.github.com> Date: Thu, 23 Jan 2025 10:12:50 +0100 Subject: [PATCH 04/11] feat: Create graphviz visualizing skript WIP --- NodeFeatures.py | 6 ++-- tests/graphviz_visualizing.py | 53 +++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 tests/graphviz_visualizing.py diff --git a/NodeFeatures.py b/NodeFeatures.py index 8505f4a..25ddd79 100644 --- a/NodeFeatures.py +++ b/NodeFeatures.py @@ -21,9 +21,9 @@ class NodeTypes(Enum): METHOD_DEFINITION = "TMethodDefinition" PARAMETER = "TParameter" # TField - FIELD = "TField" # Todo implement this in AstToEcoreConverter - FIELD_SIGNATURE = "TFieldSignature" # Todo implement this in AstToEcoreConverter - FIELD_DEFINITION = "TFieldDefinition" # Todo implement this in AstToEcoreConverter + FIELD = "TField" + FIELD_SIGNATURE = "TFieldSignature" # Todo implement this in AstToEcoreConverter (only missing TFieldSignature.type) + FIELD_DEFINITION = "TFieldDefinition" # Todo implement this in AstToEcoreConverter (missing TFieldDefinition.hidden and ".hiddenBy) # TAccess CALL = "TCall" READ = "TRead" # Todo implement this in AstToEcoreConverter diff --git a/tests/graphviz_visualizing.py b/tests/graphviz_visualizing.py new file mode 100644 index 0000000..384fb8d --- /dev/null +++ b/tests/graphviz_visualizing.py @@ -0,0 +1,53 @@ +import pandas as pd +import graphviz + +def read_node_features(file_path): + """Read node features from CSV file.""" + return pd.read_csv(file_path) + +def read_adjacency_list(file_path): + """Read adjacency list from CSV file.""" + return pd.read_csv(file_path, header=None, names=['source', 'target']) + +def read_edge_attributes(file_path): + """Read edge attributes from CSV file.""" + return pd.read_csv(file_path) + +def create_graph(node_features, adjacency_list, edge_attributes): + """Create a graph using Graphviz.""" + dot = graphviz.Digraph(comment='Graph Visualization') + + # Add nodes with features + for index, row in node_features.iterrows(): + node_id = row['hashed_node_name'] # Assuming this is the column name for hashed node names + node_type = row['node_type'] # Assuming this is the column name for node types + library_flag = row['library_flag'] # Assuming this is the column name for library flags + dot.node(node_id, f'{node_id}\nType: {node_type}\nLibrary: {library_flag}') + + # Add edges with attributes + for index, row in adjacency_list.iterrows(): + source = row['source'] + target = row['target'] + edge_attr = edge_attributes.iloc[index] # Assuming the order matches + edge_type = edge_attr['edge_type'] # Assuming this is the column name for edge types + dot.edge(source, target, label=edge_type) + + # Render the graph to a file and display it + dot.render('graph_visualization', format='png', cleanup=True) + dot.view() + +def main(output_name): + """Main function to read files and create graph.""" + node_features_file = f"{output_name}_nodefeatures.csv" + adjacency_list_file = f"{output_name}_A.csv" + edge_attributes_file = f"{output_name}_edge_attributes.csv" + + node_features = read_node_features(node_features_file) + adjacency_list = read_adjacency_list(adjacency_list_file) + edge_attributes = read_edge_attributes(edge_attributes_file) + + create_graph(node_features, adjacency_list, edge_attributes) + +if __name__ == "__main__": + output_name = "testing/csv_files/test_child_class" # Replace with your actual output name + main(output_name) \ No newline at end of file From f230f219d5f4565c26d270463ac80443d95c0790 Mon Sep 17 00:00:00 2001 From: TimTheHero <117226730+TimTheHero@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:44:01 +0100 Subject: [PATCH 05/11] docs: added discussed changes in NodeFeatures --- NodeFeatures.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/NodeFeatures.py b/NodeFeatures.py index 25ddd79..c61cf6d 100644 --- a/NodeFeatures.py +++ b/NodeFeatures.py @@ -17,13 +17,13 @@ class NodeTypes(Enum): CLASS = "TClass" # TMethod METHOD = "TMethod" - METHOD_SIGNATURE = "TMethodSignature" - METHOD_DEFINITION = "TMethodDefinition" + METHOD_SIGNATURE = "TMethodSignature" # missing firstParameter does not need to be implemented. + METHOD_DEFINITION = "TMethodDefinition"# missing "".overloading and "".overloadedBY does not need to be implemented. PARAMETER = "TParameter" # TField FIELD = "TField" FIELD_SIGNATURE = "TFieldSignature" # Todo implement this in AstToEcoreConverter (only missing TFieldSignature.type) - FIELD_DEFINITION = "TFieldDefinition" # Todo implement this in AstToEcoreConverter (missing TFieldDefinition.hidden and ".hiddenBy) + FIELD_DEFINITION = "TFieldDefinition" # missing TFieldDefinition.hidden and "".hiddenBy does not to be implemented # TAccess CALL = "TCall" READ = "TRead" # Todo implement this in AstToEcoreConverter @@ -33,4 +33,4 @@ class NodeTypes(Enum): INTERFACE = "TInterface" # In Python, there is no formal concept of interfaces as found in some other programming languages like Java or C#. # However, Python supports a similar concept through the use of abstract base classes (ABCs) and duck typing. - # The return on investment probably is not sufficient to justify the implementation. + # The return on investment probably is not sufficient to justify the implementation. \ No newline at end of file From b6d30835c076d85c356ebbb8929318d3ae619151 Mon Sep 17 00:00:00 2001 From: TimTheHero <117226730+TimTheHero@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:44:32 +0100 Subject: [PATCH 06/11] docs: add description about Graphviz_visualizing script --- tests/graphviz_visualizing.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/graphviz_visualizing.py b/tests/graphviz_visualizing.py index 384fb8d..0bd23c4 100644 --- a/tests/graphviz_visualizing.py +++ b/tests/graphviz_visualizing.py @@ -1,5 +1,9 @@ import pandas as pd import graphviz +""" +This script is WIP. +In our Application we have 3 csv files storing the data needed to visualize. +""" def read_node_features(file_path): """Read node features from CSV file.""" From c73d0f82fc7871393fcef00e7d0609049bd64fab Mon Sep 17 00:00:00 2001 From: TimTheHero <117226730+TimTheHero@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:42:10 +0100 Subject: [PATCH 07/11] feat: Add firstParameter to TParameter --- AstToEcoreConverter.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/AstToEcoreConverter.py b/AstToEcoreConverter.py index aed28b6..81cd5c2 100644 --- a/AstToEcoreConverter.py +++ b/AstToEcoreConverter.py @@ -1108,20 +1108,23 @@ def create_method_signature(self, method_node, name, arguments): name (str?): The name of the method. arguments (list?): The list of arguments for the method. """ - method_signature = self.create_ecore_instance( - NodeTypes.METHOD_SIGNATURE) + method_signature = self.create_ecore_instance(NodeTypes.METHOD_SIGNATURE) method = self.create_ecore_instance(NodeTypes.METHOD) method.tName = name self.graph.methods.append(method) method_signature.method = method previous = None + first_parameter = None for _ in arguments: parameter = self.create_ecore_instance(NodeTypes.PARAMETER) if previous is not None: parameter.previous = previous previous = parameter method_signature.parameters.append(parameter) + if first_parameter is None: + first_parameter = parameter + method_signature.firstParameter = first_parameter method_node.signature = method_signature @@ -1177,7 +1180,14 @@ def create_field(self, field_node, name, field_type=None) -> None: field.signature = field_signature field.tName = name - # field_signature.type = field_type #Todo currently bugged get str('datatype)') probably need TAbstractType + # Todo currently bugged get str('datatype)') probably need TAbstractType + # -> tAbstractType can not be init bec. abstract. + # -> Use TClass instead + # -> if TClass used type will not be set in xmi + # Create a TClass instance and set its instanceClass attribute + type_class = self.create_ecore_instance(NodeTypes.CLASS) + type_class.tName = field_type + field_signature.type = type_class self.graph.fields.append(field) @@ -1246,18 +1256,6 @@ def __init__(self, ecore_graph): self.names_in_scope: set = set() self.fields_per_class: dict = dict() - self.INTEGER_TYPE = self.ecore_graph.get_class_by_name("int") - self.FLOAT_TYPE = self.ecore_graph.get_class_by_name("float") - self.STRING_TYPE = self.ecore_graph.get_class_by_name("str") - self.BOOL_TYPE = self.ecore_graph.get_class_by_name("bool") - self.LIST_TYPE = self.ecore_graph.get_class_by_name("list") - self.TUPLE_TYPE = self.ecore_graph.get_class_by_name("tuple") - self.DICT_TYPE = self.ecore_graph.get_class_by_name("dict") - self.SET_TYPE = self.ecore_graph.get_class_by_name("set") - - def get_class_by_type(self, type_obj): - return self.ecore_graph.get_class_by_name(type_obj.__name__) - def visit_Import(self, node): """ Visits an import statement in the AST. From ac994aa4935ff7b84ad072ec8b9abf2b60270033 Mon Sep 17 00:00:00 2001 From: TimTheHero <117226730+TimTheHero@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:11:42 +0100 Subject: [PATCH 08/11] feat: Implementing TMethodSignature.retunType and TParameter.type --- AstToEcoreConverter.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/AstToEcoreConverter.py b/AstToEcoreConverter.py index 81cd5c2..7451ebb 100644 --- a/AstToEcoreConverter.py +++ b/AstToEcoreConverter.py @@ -1099,7 +1099,7 @@ def get_method_def_from_internal_structure(self, method_name, module): return current_method[0] return None - def create_method_signature(self, method_node, name, arguments): + def create_method_signature(self, method_node, name, arguments, return_type = None): """ Creates a method signature for a method definition. @@ -1107,16 +1107,21 @@ def create_method_signature(self, method_node, name, arguments): method_node: The method definition node. name (str?): The name of the method. arguments (list?): The list of arguments for the method. + return_type(str): the type as a string that gets returned from the func. """ method_signature = self.create_ecore_instance(NodeTypes.METHOD_SIGNATURE) method = self.create_ecore_instance(NodeTypes.METHOD) method.tName = name self.graph.methods.append(method) method_signature.method = method + return_class = self.create_ecore_instance(NodeTypes.CLASS) + return_class.tName = return_type + method_signature.returnType = return_class + previous = None first_parameter = None - for _ in arguments: + for arg in arguments: parameter = self.create_ecore_instance(NodeTypes.PARAMETER) if previous is not None: parameter.previous = previous @@ -1126,6 +1131,11 @@ def create_method_signature(self, method_node, name, arguments): first_parameter = parameter method_signature.firstParameter = first_parameter + # Add type for TParameter.type + parameter_type = self.create_ecore_instance(NodeTypes.CLASS) + parameter_type.tName = arg.annotation.id if arg.annotation else 'None' + parameter.type = parameter_type + method_node.signature = method_signature # for internal structure @@ -1376,10 +1386,24 @@ def visit_FunctionDef(self, node): node.name, self.current_class) if self.current_method is None: self.current_class = None + return_type = None + for statement in node.body: + if isinstance(statement, ast.Return): + if isinstance(statement.value, ast.Constant): + return_type = type(statement.value.value).__name__ + elif isinstance(statement.value, ast.Call): + if isinstance(statement.value.func, ast.Name): + return_type = statement.value.func.id + elif isinstance(statement.value.func, ast.Attribute): + return_type = statement.value.func.attr + elif isinstance(statement.value, ast.Name): + return_type = statement.value.id + elif isinstance(statement.value, ast.Attribute): + return_type = statement.value.attr self.current_method = self.ecore_graph.create_ecore_instance( NodeTypes.METHOD_DEFINITION) self.ecore_graph.create_method_signature( - self.current_method, node.name, node.args.args) + self.current_method, node.name, node.args.args, return_type) module_node = self.ecore_graph.get_current_module() self.current_module = module_node module_node.contains.append(self.current_method) From 6f0fff5d50257c6e3b782f08a826274443c0425b Mon Sep 17 00:00:00 2001 From: TimTheHero <117226730+TimTheHero@users.noreply.github.com> Date: Sat, 8 Feb 2025 13:18:04 +0100 Subject: [PATCH 09/11] testing: Adding test_assignments_in_class to minimal examples --- .../minimal_examples/field_assignments/my_module.py | 7 +++++++ tests/test_minimal_examples.py | 12 +++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 tests/minimal_examples/field_assignments/my_module.py diff --git a/tests/minimal_examples/field_assignments/my_module.py b/tests/minimal_examples/field_assignments/my_module.py new file mode 100644 index 0000000..bc5a464 --- /dev/null +++ b/tests/minimal_examples/field_assignments/my_module.py @@ -0,0 +1,7 @@ +class MyClass: + def __init__(self): + self.my_field = 5 + self.my_field_2 = "Hi" + + a = 5 + b = "Hallo" \ No newline at end of file diff --git a/tests/test_minimal_examples.py b/tests/test_minimal_examples.py index 0d9c72d..5c13e4a 100644 --- a/tests/test_minimal_examples.py +++ b/tests/test_minimal_examples.py @@ -63,10 +63,20 @@ def test_pyinputplus(self): def test_assignment(self) : """ - + This test tests several assignments. """ repo = 'minimal_examples/assignments' check_path_exists(repo) resource_set = ResourceSet() graph = ProjectEcoreGraph(resource_set, repo, True, test_output_dir) ecore_graph = graph.get_graph() + + def test_assignments_in_class(self): + """ + This test tests several assignments in a class. + """ + repo = 'minimal_examples/field_assignments' + check_path_exists(repo) + resource_set = ResourceSet() + graph = ProjectEcoreGraph(resource_set, repo, True, test_output_dir) + ecore_graph = graph.get_graph() From 7e91e331fe2250942aa0140eee5691262b8f9613 Mon Sep 17 00:00:00 2001 From: TimTheHero <117226730+TimTheHero@users.noreply.github.com> Date: Sat, 8 Feb 2025 13:20:16 +0100 Subject: [PATCH 10/11] fix/feature: fix bug where a field in defined in a class --- AstToEcoreConverter.py | 21 ++- .../ast_to_ecore_minimal_example.py | 136 ++++++++++++++++++ 2 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 tests/minimal_examples/ast_to_ecore_minimal_example.py diff --git a/AstToEcoreConverter.py b/AstToEcoreConverter.py index 7451ebb..f185a05 100644 --- a/AstToEcoreConverter.py +++ b/AstToEcoreConverter.py @@ -1118,6 +1118,7 @@ def create_method_signature(self, method_node, name, arguments, return_type = No return_class.tName = return_type method_signature.returnType = return_class + method_node.returnType = return_class previous = None first_parameter = None @@ -1205,8 +1206,6 @@ def create_field(self, field_node, name, field_type=None) -> None: module_node = self.get_current_module() self.field_list.append([field_node, name, field_type, module_node]) - field_node.contains.append(field_definition) - def get_field_from_internal_structure(self, field_name, module=None): """ Retrieves a field from the internal structure by name and module. @@ -1442,9 +1441,21 @@ def visit_Assign(self, node): elif isinstance(node.value, ast.Attribute): field_type = node.value.attr self.ecore_graph.create_field(self.current_class, field_name, field_type) - if self.current_class not in self.fields_per_class: - self.fields_per_class[self.current_class] = set() - self.fields_per_class[self.current_class].add(target.attr) + elif isinstance(target, ast.Name): + field_name = target.id + field_type = None + if isinstance(node.value, ast.Constant): + field_type = type(node.value.value).__name__ + elif isinstance(node.value, ast.Call): + if isinstance(node.value.func, ast.Name): + field_type = node.value.func.id + elif isinstance(node.value.func, ast.Attribute): + field_type = node.value.func.attr + elif isinstance(node.value, ast.Name): + field_type = node.value.id + elif isinstance(node.value, ast.Attribute): + field_type = node.value.attr + self.ecore_graph.create_field(self.current_class, field_name, field_type) # Find all module-level variables assignments: if self.current_class is None: diff --git a/tests/minimal_examples/ast_to_ecore_minimal_example.py b/tests/minimal_examples/ast_to_ecore_minimal_example.py new file mode 100644 index 0000000..0875eba --- /dev/null +++ b/tests/minimal_examples/ast_to_ecore_minimal_example.py @@ -0,0 +1,136 @@ +import ast + + +class ProjectEcoreGraph: + def __init__(self): + self.graph = None + self.current_module = None + self.field_list = [] + + def create_field(self, field_node, name, field_type=None): + field = Field() + field.name = name + field.type = field_type + field_node.fields.append(field) + self.field_list.append([field_node, name, field_type]) + + def get_field_from_internal_structure(self, field_name, module=None): + for current_field in self.field_list: + if field_name == current_field[1]: + if module is None and current_field[2] is None: + return current_field[0] + if hasattr(module, 'location') and hasattr(current_field[2], 'location'): + if module.location == current_field[2].location: + return current_field[0] + return None + + +class Field: + def __init__(self): + self.name = None + self.type = None + + +class Module: + def __init__(self): + self.fields = [] + + +class ASTVisitor(ast.NodeVisitor): + def __init__(self, ecore_graph): + self.ecore_graph = ecore_graph + self.current_module = None + self.current_class = None + + def visit_Assign(self, node): + if self.current_class is not None: + for target in node.targets: + if isinstance(target, ast.Attribute): + if isinstance(target.value, ast.Name): + if target.value.id == 'self': + field_name = target.attr + field_type = None + if isinstance(node.value, ast.Constant): + field_type = type(node.value.value).__name__ + elif isinstance(node.value, ast.Call): + if isinstance(node.value.func, ast.Name): + field_type = node.value.func.id + elif isinstance(node.value.func, ast.Attribute): + field_type = node.value.func.attr + elif isinstance(node.value, ast.Name): + field_type = node.value.id + elif isinstance(node.value, ast.Attribute): + field_type = node.value.attr + self.ecore_graph.create_field(self.current_class, field_name, field_type) + elif isinstance(target, ast.Name): + field_name = target.id + field_type = None + if isinstance(node.value, ast.Constant): + field_type = type(node.value.value).__name__ + elif isinstance(node.value, ast.Call): + if isinstance(node.value.func, ast.Name): + field_type = node.value.func.id + elif isinstance(node.value.func, ast.Attribute): + field_type = node.value.func.attr + elif isinstance(node.value, ast.Name): + field_type = node.value.id + elif isinstance(node.value, ast.Attribute): + field_type = node.value.attr + self.ecore_graph.create_field(self.current_class, field_name, field_type) + self.generic_visit(node) + + def visit_ClassDef(self, node): + self.current_class = Module() + self.generic_visit(node) + + +class VarVisitor(ast.NodeVisitor): + def __init__(self, var_name): + self.var_name = var_name + self.found = False + + def visit_Assign(self, node): + for target in node.targets: + if isinstance(target, ast.Name) and target.id == self.var_name: + self.found = True + + def visit_FunctionDef(self, node): + for statement in node.body: + self.visit(statement) + + def visit_ClassDef(self, node): + for statement in node.body: + self.visit(statement) + + +def check_var_in_class(code, var_name): + tree = ast.parse(code) + visitor = VarVisitor(var_name) + visitor.visit(tree) + return visitor.found + + +# Example usage +ecore_graph = ProjectEcoreGraph() +visitor = ASTVisitor(ecore_graph) + +code = """ +class MyClass: + def __init__(self): + self.my_field = 5 + self.my_field_2 = "Hi" + + a = 5 + b = "Hallo" +""" + +tree = ast.parse(code) +visitor.visit(tree) + +print(ecore_graph.field_list) + +var_name = "a" +if check_var_in_class(code, var_name): + print(f"Variable {var_name} found in class") +else: + print(f"Variable {var_name} not found in class") \ No newline at end of file From 760eaf37ea7e2166e67e71093a186ba188bcd271 Mon Sep 17 00:00:00 2001 From: TimTheHero <117226730+TimTheHero@users.noreply.github.com> Date: Sat, 8 Feb 2025 13:20:26 +0100 Subject: [PATCH 11/11] Update .coverage --- tests/.coverage | Bin 53248 -> 53248 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/.coverage b/tests/.coverage index d7c025c6bebda70bfe3d0dc14212b636ae98c860..84d757f6544033b9f71c98497b20ccd19cefe2c9 100644 GIT binary patch delta 693 zcmZuuQAkr^6h8mmojULR|KFjwp4^~^@S(P+gwQ5EDan$ghaeOy5y|DEhi2*Sz1pi4 z3@NDG-VCBLhG8ge5JA*~wJ_?TZV#zXI%V43+;&gDkZ&D0eCK=|KE88~o^Ntwb5NVPb1W?{#5U(9m-D1UH3_;I?H4*n&Y9aKwLSa)Q5+z4u$)N z`v#)@;mGj7^<$CIR7cm6qlqcIz%R3t^pAQ^?WC{hHSVPWKF4mWrm~yzb$?NuJ+jeE z4x}3XBgImk7Zwlu7@YwAp6#(sw#vS;mn_Mq*%TY6dvu1z>4b3BOWmBw#N}h5?76Y+ zcOG@S5`Q)|s2QL*iE9FsP}c8^0f6BJu+4;qxinf>Nn<{T6@l7j0xiup1!)&8j0G`g zicIgb0VmcRePhs69HamzKkZT&+4Jk@mj|NcI$H|f(G}Crwj=-b@ z68Hcl-CKSDn<-t2kwbb5aHp>ua4&eDW|Rf6jE`cfy{*3BS&%p>6J088V3;7Dsyvl! zM}hptE2zE63?jA;u6>t|6WQg8gt9$`x2{0!9OlqUYq*R>v?~eBC-6iufkn;E2QiOD zvDrh-7HwaxFwJ-pc!}@u4Zg-r{*8a;ANX6o#OL{QKFgEhy<@z(N*s?cmYPkR5Bvq$ C2Re%Y delta 569 zcmXX@OK1~e5dL>J(Zt>VpDNMZQbDjeHMyv#HaR9BRYdTjR(z0)__)}JrvJtV9z-hy z4?@Z2B0bo&g-|K9kcg*xFj!G~a&vM)hzYEDb+bGEwa)XK;e(lB^pc{NloekxLbViu zWw-_r_KiJe6SPBD=^Yx=erR)As&a7TxK>G>-cSTFP2f74q+RWmmZtCNT^OZtSYb~z zlYJM&F|CrmxPB}`X_>$#jKDtTvfpfzt+6tD%U-h(U87||d`=667a7h~zZA^Rs#9Ar ziTo2CdzOh#3tf&a96B6DHXI9G1AFLJx#zB%^{UgW3zFyxyfcBnOnce;pLQV^-JEcf z7p-a6E%HuIfSh3sE?}Z=58ol)FIodRJYLAXyv@%^0mUZ;4=GpMkUuGlogUqkmp%uG zJ)Ai;CXt$CtDSp@HRKKBLC>?Xin&pJ|01bp2+>JQM>#9algqO>qx8F-=Cd7cU((fV zo)`Dd+PjS;^`sPv>fzc~**-(I!!<{4H>WvyD95AZC?T>4{SR}&#-ti;gmo#aw9gXR z-*;~)ZF!trk*l@C*+2tGsNcmSeN=}ykPR2@`8OH$gyahbZ%u8fVpt7bp8`~(xW;R}3%kFWu¨U@VNan@GW?ZA H<9z%tlbj5#