]> git.proxmox.com Git - mirror_edk2.git/blame - .pytool/Plugin/EccCheck/EccCheck.py
.pytool/EccCheck: Check ecc_csv exists
[mirror_edk2.git] / .pytool / Plugin / EccCheck / EccCheck.py
CommitLineData
fbc9cb4c
SZ
1# @file EccCheck.py\r
2#\r
22fe311b 3# Copyright (c) 2021, Arm Limited. All rights reserved.<BR>\r
fbc9cb4c
SZ
4# Copyright (c) 2020, Intel Corporation. All rights reserved.<BR>\r
5# SPDX-License-Identifier: BSD-2-Clause-Patent\r
6##\r
7\r
8import os\r
9import shutil\r
10import re\r
11import csv\r
12import xml.dom.minidom\r
13from typing import List, Dict, Tuple\r
14import logging\r
15from io import StringIO\r
16from edk2toolext.environment import shell_environment\r
17from edk2toolext.environment.plugintypes.ci_build_plugin import ICiBuildPlugin\r
18from edk2toolext.environment.var_dict import VarDict\r
19from edk2toollib.utility_functions import RunCmd\r
20\r
21\r
22class EccCheck(ICiBuildPlugin):\r
23 """\r
24 A CiBuildPlugin that finds the Ecc issues of newly added code in pull request.\r
25\r
26 Configuration options:\r
27 "EccCheck": {\r
28 "ExceptionList": [],\r
29 "IgnoreFiles": []\r
30 },\r
31 """\r
32\r
33 ReModifyFile = re.compile(r'[B-Q,S-Z]+[\d]*\t(.*)')\r
34 FindModifyFile = re.compile(r'\+\+\+ b\/(.*)')\r
35 LineScopePattern = (r'@@ -\d*\,*\d* \+\d*\,*\d* @@.*')\r
36 LineNumRange = re.compile(r'@@ -\d*\,*\d* \+(\d*)\,*(\d*) @@.*')\r
37\r
38 def GetTestName(self, packagename: str, environment: VarDict) -> tuple:\r
39 """ Provide the testcase name and classname for use in reporting\r
40 testclassname: a descriptive string for the testcase can include whitespace\r
41 classname: should be patterned <packagename>.<plugin>.<optionally any unique condition>\r
42\r
43 Args:\r
44 packagename: string containing name of package to build\r
45 environment: The VarDict for the test to run in\r
46 Returns:\r
47 a tuple containing the testcase name and the classname\r
48 (testcasename, classname)\r
49 """\r
50 return ("Check for efi coding style for " + packagename, packagename + ".EccCheck")\r
51\r
52 ##\r
53 # External function of plugin. This function is used to perform the task of the ci_build_plugin Plugin\r
54 #\r
55 # - package is the edk2 path to package. This means workspace/packagepath relative.\r
56 # - edk2path object configured with workspace and packages path\r
57 # - PkgConfig Object (dict) for the pkg\r
58 # - EnvConfig Object\r
59 # - Plugin Manager Instance\r
60 # - Plugin Helper Obj Instance\r
61 # - Junit Logger\r
62 # - output_stream the StringIO output stream from this plugin via logging\r
63 def RunBuildPlugin(self, packagename, Edk2pathObj, pkgconfig, environment, PLM, PLMHelper, tc, output_stream=None):\r
a050c599 64 workspace_path = Edk2pathObj.WorkspacePath\r
22fe311b
PG
65 basetools_path = environment.GetValue("EDK_TOOLS_PATH")\r
66 python_path = os.path.join(basetools_path, "Source", "Python")\r
fbc9cb4c
SZ
67 env = shell_environment.GetEnvironment()\r
68 env.set_shell_var('PYTHONPATH', python_path)\r
a050c599 69 env.set_shell_var('WORKSPACE', workspace_path)\r
fbc9cb4c 70 self.ECC_PASS = True\r
a050c599 71 self.ApplyConfig(pkgconfig, workspace_path, basetools_path, packagename)\r
fbc9cb4c
SZ
72 modify_dir_list = self.GetModifyDir(packagename)\r
73 patch = self.GetDiff(packagename)\r
a050c599
PG
74 ecc_diff_range = self.GetDiffRange(patch, packagename, workspace_path)\r
75 self.GenerateEccReport(modify_dir_list, ecc_diff_range, workspace_path, basetools_path)\r
76 ecc_log = os.path.join(workspace_path, "Ecc.log")\r
fbc9cb4c
SZ
77 self.RevertCode()\r
78 if self.ECC_PASS:\r
79 tc.SetSuccess()\r
80 self.RemoveFile(ecc_log)\r
81 return 0\r
82 else:\r
83 with open(ecc_log, encoding='utf8') as output:\r
84 ecc_output = output.readlines()\r
85 for line in ecc_output:\r
86 logging.error(line.strip())\r
87 self.RemoveFile(ecc_log)\r
88 tc.SetFailed("EccCheck failed for {0}".format(packagename), "Ecc detected issues")\r
89 return 1\r
90\r
91 def RevertCode(self) -> None:\r
92 submoudle_params = "submodule update --init"\r
93 RunCmd("git", submoudle_params)\r
94 reset_params = "reset HEAD --hard"\r
95 RunCmd("git", reset_params)\r
96\r
97 def GetDiff(self, pkg: str) -> List[str]:\r
98 return_buffer = StringIO()\r
99 params = "diff --unified=0 origin/master HEAD"\r
100 RunCmd("git", params, outstream=return_buffer)\r
101 p = return_buffer.getvalue().strip()\r
102 patch = p.split("\n")\r
103 return_buffer.close()\r
104\r
105 return patch\r
106\r
107 def RemoveFile(self, file: str) -> None:\r
108 if os.path.exists(file):\r
109 os.remove(file)\r
110 return\r
111\r
112 def GetModifyDir(self, pkg: str) -> List[str]:\r
113 return_buffer = StringIO()\r
114 params = "diff --name-status" + ' HEAD' + ' origin/master'\r
115 RunCmd("git", params, outstream=return_buffer)\r
116 p1 = return_buffer.getvalue().strip()\r
117 dir_list = p1.split("\n")\r
118 return_buffer.close()\r
119 modify_dir_list = []\r
120 for modify_dir in dir_list:\r
121 file_path = self.ReModifyFile.findall(modify_dir)\r
122 if file_path:\r
123 file_dir = os.path.dirname(file_path[0])\r
124 else:\r
125 continue\r
126 if pkg in file_dir and file_dir != pkg:\r
127 modify_dir_list.append('%s' % file_dir)\r
128 else:\r
129 continue\r
130\r
131 modify_dir_list = list(set(modify_dir_list))\r
132 return modify_dir_list\r
133\r
134 def GetDiffRange(self, patch_diff: List[str], pkg: str, workingdir: str) -> Dict[str, List[Tuple[int, int]]]:\r
135 IsDelete = True\r
136 StartCheck = False\r
137 range_directory: Dict[str, List[Tuple[int, int]]] = {}\r
138 for line in patch_diff:\r
139 modify_file = self.FindModifyFile.findall(line)\r
140 if modify_file and pkg in modify_file[0] and not StartCheck and os.path.isfile(modify_file[0]):\r
141 modify_file_comment_dic = self.GetCommentRange(modify_file[0], workingdir)\r
142 IsDelete = False\r
143 StartCheck = True\r
144 modify_file_dic = modify_file[0]\r
145 modify_file_dic = modify_file_dic.replace("/", os.sep)\r
146 range_directory[modify_file_dic] = []\r
147 elif line.startswith('--- '):\r
148 StartCheck = False\r
149 elif re.match(self.LineScopePattern, line, re.I) and not IsDelete and StartCheck:\r
150 start_line = self.LineNumRange.search(line).group(1)\r
151 line_range = self.LineNumRange.search(line).group(2)\r
152 if not line_range:\r
153 line_range = '1'\r
154 range_directory[modify_file_dic].append((int(start_line), int(start_line) + int(line_range) - 1))\r
155 for i in modify_file_comment_dic:\r
156 if int(i[0]) <= int(start_line) <= int(i[1]):\r
157 range_directory[modify_file_dic].append(i)\r
158 return range_directory\r
159\r
160 def GetCommentRange(self, modify_file: str, workingdir: str) -> List[Tuple[int, int]]:\r
161 modify_file_path = os.path.join(workingdir, modify_file)\r
162 with open(modify_file_path) as f:\r
163 line_no = 1\r
164 comment_range: List[Tuple[int, int]] = []\r
165 Start = False\r
166 for line in f:\r
167 if line.startswith('/**'):\r
168 start_no = line_no\r
169 Start = True\r
170 if line.startswith('**/') and Start:\r
171 end_no = line_no\r
172 Start = False\r
173 comment_range.append((int(start_no), int(end_no)))\r
174 line_no += 1\r
175\r
176 if comment_range and comment_range[0][0] == 1:\r
177 del comment_range[0]\r
178 return comment_range\r
179\r
180 def GenerateEccReport(self, modify_dir_list: List[str], ecc_diff_range: Dict[str, List[Tuple[int, int]]],\r
a050c599 181 workspace_path: str, basetools_path: str) -> None:\r
fbc9cb4c
SZ
182 ecc_need = False\r
183 ecc_run = True\r
22fe311b
PG
184 config = os.path.join(basetools_path, "Source", "Python", "Ecc", "config.ini")\r
185 exception = os.path.join(basetools_path, "Source", "Python", "Ecc", "exception.xml")\r
a050c599 186 report = os.path.join(workspace_path, "Ecc.csv")\r
fbc9cb4c 187 for modify_dir in modify_dir_list:\r
a050c599 188 target = os.path.join(workspace_path, modify_dir)\r
fbc9cb4c
SZ
189 logging.info('Run ECC tool for the commit in %s' % modify_dir)\r
190 ecc_need = True\r
191 ecc_params = "-c {0} -e {1} -t {2} -r {3}".format(config, exception, target, report)\r
a050c599 192 return_code = RunCmd("Ecc", ecc_params, workingdir=workspace_path)\r
fbc9cb4c
SZ
193 if return_code != 0:\r
194 ecc_run = False\r
195 break\r
196 if not ecc_run:\r
197 logging.error('Fail to run ECC tool')\r
a050c599 198 self.ParseEccReport(ecc_diff_range, workspace_path)\r
fbc9cb4c
SZ
199\r
200 if not ecc_need:\r
201 logging.info("Doesn't need run ECC check")\r
202\r
203 revert_params = "checkout -- {}".format(exception)\r
204 RunCmd("git", revert_params)\r
205 return\r
206\r
a050c599
PG
207 def ParseEccReport(self, ecc_diff_range: Dict[str, List[Tuple[int, int]]], workspace_path: str) -> None:\r
208 ecc_log = os.path.join(workspace_path, "Ecc.log")\r
50672d26 209 ecc_csv = os.path.join(workspace_path, "Ecc.csv")\r
fbc9cb4c
SZ
210 row_lines = []\r
211 ignore_error_code = self.GetIgnoreErrorCode()\r
50672d26 212 if os.path.exists(ecc_csv):\r
fbc9cb4c
SZ
213 with open(ecc_csv) as csv_file:\r
214 reader = csv.reader(csv_file)\r
215 for row in reader:\r
216 for modify_file in ecc_diff_range:\r
217 if modify_file in row[3]:\r
218 for i in ecc_diff_range[modify_file]:\r
219 line_no = int(row[4])\r
220 if i[0] <= line_no <= i[1] and row[1] not in ignore_error_code:\r
221 row[0] = '\nEFI coding style error'\r
222 row[1] = 'Error code: ' + row[1]\r
223 row[3] = 'file: ' + row[3]\r
224 row[4] = 'Line number: ' + row[4]\r
225 row_line = '\n *'.join(row)\r
226 row_lines.append(row_line)\r
227 break\r
228 break\r
229 if row_lines:\r
230 self.ECC_PASS = False\r
231\r
232 with open(ecc_log, 'a') as log:\r
233 all_line = '\n'.join(row_lines)\r
234 all_line = all_line + '\n'\r
235 log.writelines(all_line)\r
236 return\r
237\r
a050c599 238 def ApplyConfig(self, pkgconfig: Dict[str, List[str]], workspace_path: str, basetools_path: str, pkg: str) -> None:\r
fbc9cb4c
SZ
239 if "IgnoreFiles" in pkgconfig:\r
240 for a in pkgconfig["IgnoreFiles"]:\r
a050c599 241 a = os.path.join(workspace_path, pkg, a)\r
fbc9cb4c
SZ
242 a = a.replace(os.sep, "/")\r
243\r
244 logging.info("Ignoring Files {0}".format(a))\r
245 if os.path.exists(a):\r
246 if os.path.isfile(a):\r
247 self.RemoveFile(a)\r
248 elif os.path.isdir(a):\r
249 shutil.rmtree(a)\r
250 else:\r
251 logging.error("EccCheck.IgnoreInf -> {0} not found in filesystem. Invalid ignore files".format(a))\r
252\r
253 if "ExceptionList" in pkgconfig:\r
254 exception_list = pkgconfig["ExceptionList"]\r
22fe311b 255 exception_xml = os.path.join(basetools_path, "Source", "Python", "Ecc", "exception.xml")\r
fbc9cb4c
SZ
256 try:\r
257 logging.info("Appending exceptions")\r
258 self.AppendException(exception_list, exception_xml)\r
259 except Exception as e:\r
260 logging.error("Fail to apply exceptions")\r
261 raise e\r
262 return\r
263\r
264 def AppendException(self, exception_list: List[str], exception_xml: str) -> None:\r
265 error_code_list = exception_list[::2]\r
266 keyword_list = exception_list[1::2]\r
267 dom_tree = xml.dom.minidom.parse(exception_xml)\r
268 root_node = dom_tree.documentElement\r
269 for error_code, keyword in zip(error_code_list, keyword_list):\r
270 customer_node = dom_tree.createElement("Exception")\r
271 keyword_node = dom_tree.createElement("KeyWord")\r
272 keyword_node_text_value = dom_tree.createTextNode(keyword)\r
273 keyword_node.appendChild(keyword_node_text_value)\r
274 customer_node.appendChild(keyword_node)\r
275 error_code_node = dom_tree.createElement("ErrorID")\r
276 error_code_text_value = dom_tree.createTextNode(error_code)\r
277 error_code_node.appendChild(error_code_text_value)\r
278 customer_node.appendChild(error_code_node)\r
279 root_node.appendChild(customer_node)\r
280 with open(exception_xml, 'w') as f:\r
281 dom_tree.writexml(f, indent='', addindent='', newl='\n', encoding='UTF-8')\r
282 return\r
283\r
284 def GetIgnoreErrorCode(self) -> set:\r
285 """\r
286 Below are kinds of error code that are accurate in ecc scanning of edk2 level.\r
287 But EccCheck plugin is partial scanning so they are always false positive issues.\r
288 The mapping relationship of error code and error message is listed BaseTools/Sourc/Python/Ecc/EccToolError.py\r
289 """\r
290 ignore_error_code = {\r
291 "10000",\r
292 "10001",\r
293 "10002",\r
294 "10003",\r
295 "10004",\r
296 "10005",\r
297 "10006",\r
298 "10007",\r
299 "10008",\r
300 "10009",\r
301 "10010",\r
302 "10011",\r
303 "10012",\r
304 "10013",\r
305 "10015",\r
306 "10016",\r
307 "10017",\r
308 "10022",\r
309 }\r
310 return ignore_error_code\r