1#!/usr/bin/env python3
2
3import os
4from .unified_format_parser import UnifiedFormatParser, ChangeSet
5
6
7class ReportError(Exception):
8    pass
9
10
11class Report(object):
12    class ReportEntry:
13        def __init__(self, file_path, line_number, entry_text, line_id):
14            # type: (str, int, list, int) -> None
15            if not isinstance(line_number, int) or \
16               not isinstance(line_id, int):
17                raise ReportError("ReportEntry constructor wrong type args")
18            self.file_path = file_path
19            self.line_number = line_number
20            self.text = entry_text
21            self.line_id = line_id
22
23    def __init__(self, report_path):
24        # type: (str) -> None
25        self.__entries = {}
26        self.__path = report_path
27        self.__last_line_order = 0
28
29    def parse(self):
30        # type: () -> None
31        raise ReportError("Please create a specialised class from 'Report'.")
32
33    def get_report_path(self):
34        # type: () -> str
35        return self.__path
36
37    def get_report_entries(self):
38        # type: () -> dict
39        return self.__entries
40
41    def add_entry(self, entry_path, entry_line_number, entry_text):
42        # type: (str, int, str) -> None
43        entry = Report.ReportEntry(entry_path, entry_line_number, entry_text,
44                                   self.__last_line_order)
45        if entry_path in self.__entries.keys():
46            self.__entries[entry_path].append(entry)
47        else:
48            self.__entries[entry_path] = [entry]
49        self.__last_line_order += 1
50
51    def remove_entries(self, entry_file_path):
52        # type: (str) -> None
53        del self.__entries[entry_file_path]
54
55    def remove_entry(self, entry_path, line_id):
56        # type: (str, int) -> None
57        if entry_path in self.__entries.keys():
58            len_entry_path = len(self.__entries[entry_path])
59            if len_entry_path == 1:
60                del self.__entries[entry_path]
61            else:
62                if line_id in self.__entries[entry_path]:
63                    self.__entries[entry_path].remove(line_id)
64
65    def patch(self, diff_obj):
66        # type: (UnifiedFormatParser) -> Report
67        filename, file_extension = os.path.splitext(self.__path)
68        patched_report = self.__class__(filename + ".patched" + file_extension)
69        remove_files = []
70        rename_files = []
71        remove_entry = []
72        ChangeMode = ChangeSet.ChangeMode
73
74        # Copy entries from this report to the report we are going to patch
75        for entries in self.__entries.values():
76            for entry in entries:
77                patched_report.add_entry(entry.file_path, entry.line_number,
78                                         entry.text)
79
80        # Patch the output report
81        patched_rep_entries = patched_report.get_report_entries()
82        for file_diff, change_obj in diff_obj.get_change_sets().items():
83            if change_obj.is_change_mode(ChangeMode.COPY):
84                # Copy the original entry pointed by change_obj.orig_file into
85                # a new key in the patched report named change_obj.dst_file,
86                # that here is file_diff variable content, because this
87                # change_obj is pushed into the change_sets with the
88                # change_obj.dst_file key
89                if change_obj.orig_file in self.__entries.keys():
90                    for entry in self.__entries[change_obj.orig_file]:
91                        patched_report.add_entry(file_diff,
92                                                 entry.line_number,
93                                                 entry.text)
94
95            if file_diff in patched_rep_entries.keys():
96                if change_obj.is_change_mode(ChangeMode.DELETE):
97                    # No need to check changes here, just remember to delete
98                    # the file from the report
99                    remove_files.append(file_diff)
100                    continue
101                elif change_obj.is_change_mode(ChangeMode.RENAME):
102                    # Remember to rename the file entry on this report
103                    rename_files.append(change_obj)
104
105                for line_num, change_type in change_obj.get_change_set():
106                    len_rep = len(patched_rep_entries[file_diff])
107                    for i in range(len_rep):
108                        rep_item = patched_rep_entries[file_diff][i]
109                        if change_type == ChangeSet.ChangeType.REMOVE:
110                            if rep_item.line_number == line_num:
111                                # This line is removed with this changes,
112                                # append to the list of entries to be removed
113                                remove_entry.append(rep_item)
114                            elif rep_item.line_number > line_num:
115                                rep_item.line_number -= 1
116                        elif change_type == ChangeSet.ChangeType.ADD:
117                            if rep_item.line_number >= line_num:
118                                rep_item.line_number += 1
119                    # Remove deleted entries from the list
120                    if len(remove_entry) > 0:
121                        for entry in remove_entry:
122                            patched_report.remove_entry(entry.file_path,
123                                                        entry.line_id)
124                        del remove_entry[:]
125
126        if len(remove_files) > 0:
127            for file_name in remove_files:
128                patched_report.remove_entries(file_name)
129
130        if len(rename_files) > 0:
131            for change_obj in rename_files:
132                patched_rep_entries[change_obj.dst_file] = \
133                    patched_rep_entries.pop(change_obj.orig_file)
134
135        return patched_report
136
137    def to_list(self):
138        # type: () -> list
139        report_list = []
140        for _, entries in self.__entries.items():
141            for entry in entries:
142                report_list.append(entry)
143
144        report_list.sort(key=lambda x: x.line_id)
145        return report_list
146
147    def __str__(self):
148        # type: () -> str
149        ret = ""
150        for entry in self.to_list():
151            ret += entry.file_path + ":" + entry.line_number + ":" + entry.text
152
153        return ret
154
155    def __len__(self):
156        # type: () -> int
157        return len(self.to_list())
158
159    def __sub__(self, report_b):
160        # type: (Report) -> Report
161        if self.__class__ != report_b.__class__:
162            raise ReportError("Diff of different type of report!")
163
164        filename, file_extension = os.path.splitext(self.__path)
165        diff_report = self.__class__(filename + ".diff" + file_extension)
166        # Put in the diff report only records of this report that are not
167        # present in the report_b.
168        for file_path, entries in self.__entries.items():
169            rep_b_entries = report_b.get_report_entries()
170            if file_path in rep_b_entries.keys():
171                # File path exists in report_b, so check what entries of that
172                # file path doesn't exist in report_b and add them to the diff
173                rep_b_entries_num = [
174                    x.line_number for x in rep_b_entries[file_path]
175                ]
176                for entry in entries:
177                    if entry.line_number not in rep_b_entries_num:
178                        diff_report.add_entry(file_path, entry.line_number,
179                                              entry.text)
180            else:
181                # File path doesn't exist in report_b, so add every entry
182                # of that file path to the diff
183                for entry in entries:
184                    diff_report.add_entry(file_path, entry.line_number,
185                                          entry.text)
186
187        return diff_report
188