1 | # @file SpellCheck.py
|
---|
2 | #
|
---|
3 | # An edk2-pytool based plugin wrapper for cspell
|
---|
4 | #
|
---|
5 | # Copyright (c) Microsoft Corporation.
|
---|
6 | # SPDX-License-Identifier: BSD-2-Clause-Patent
|
---|
7 | ##
|
---|
8 | import logging
|
---|
9 | import json
|
---|
10 | import yaml
|
---|
11 | from io import StringIO
|
---|
12 | import os
|
---|
13 | from edk2toolext.environment.plugintypes.ci_build_plugin import ICiBuildPlugin
|
---|
14 | from edk2toollib.utility_functions import RunCmd
|
---|
15 | from edk2toolext.environment.var_dict import VarDict
|
---|
16 | from edk2toollib.gitignore_parser import parse_gitignore_lines
|
---|
17 | from edk2toolext.environment import version_aggregator
|
---|
18 |
|
---|
19 |
|
---|
20 | class SpellCheck(ICiBuildPlugin):
|
---|
21 | """
|
---|
22 | A CiBuildPlugin that uses the cspell node module to scan the files
|
---|
23 | from the package being tested for spelling errors. The plugin contains
|
---|
24 | the base cspell.json file then thru the configuration options other settings
|
---|
25 | can be changed or extended.
|
---|
26 |
|
---|
27 | Configuration options:
|
---|
28 | "SpellCheck": {
|
---|
29 | "AuditOnly": False, # Don't fail the build if there are errors. Just log them
|
---|
30 | "IgnoreFiles": [], # use gitignore syntax to ignore errors in matching files
|
---|
31 | "ExtendWords": [], # words to extend to the dictionary for this package
|
---|
32 | "IgnoreStandardPaths": [], # Standard Plugin defined paths that should be ignore
|
---|
33 | "AdditionalIncludePaths": [] # Additional paths to spell check (wildcards supported)
|
---|
34 | }
|
---|
35 | """
|
---|
36 |
|
---|
37 | #
|
---|
38 | # A package can remove any of these using IgnoreStandardPaths
|
---|
39 | #
|
---|
40 | STANDARD_PLUGIN_DEFINED_PATHS = ("*.c", "*.h",
|
---|
41 | "*.nasm", "*.asm", "*.masm", "*.s",
|
---|
42 | "*.asl",
|
---|
43 | "*.dsc", "*.dec", "*.fdf", "*.inf",
|
---|
44 | "*.md", "*.txt"
|
---|
45 | )
|
---|
46 |
|
---|
47 | def GetTestName(self, packagename: str, environment: VarDict) -> tuple:
|
---|
48 | """ Provide the testcase name and classname for use in reporting
|
---|
49 |
|
---|
50 | Args:
|
---|
51 | packagename: string containing name of package to build
|
---|
52 | environment: The VarDict for the test to run in
|
---|
53 | Returns:
|
---|
54 | a tuple containing the testcase name and the classname
|
---|
55 | (testcasename, classname)
|
---|
56 | testclassname: a descriptive string for the testcase can include whitespace
|
---|
57 | classname: should be patterned <packagename>.<plugin>.<optionally any unique condition>
|
---|
58 | """
|
---|
59 | return ("Spell check files in " + packagename, packagename + ".SpellCheck")
|
---|
60 |
|
---|
61 | ##
|
---|
62 | # External function of plugin. This function is used to perform the task of the CiBuild Plugin
|
---|
63 | #
|
---|
64 | # - package is the edk2 path to package. This means workspace/packagepath relative.
|
---|
65 | # - edk2path object configured with workspace and packages path
|
---|
66 | # - PkgConfig Object (dict) for the pkg
|
---|
67 | # - EnvConfig Object
|
---|
68 | # - Plugin Manager Instance
|
---|
69 | # - Plugin Helper Obj Instance
|
---|
70 | # - Junit Logger
|
---|
71 | # - output_stream the StringIO output stream from this plugin via logging
|
---|
72 |
|
---|
73 | def RunBuildPlugin(self, packagename, Edk2pathObj, pkgconfig, environment, PLM, PLMHelper, tc, output_stream=None):
|
---|
74 | Errors = []
|
---|
75 |
|
---|
76 | abs_pkg_path = Edk2pathObj.GetAbsolutePathOnThisSystemFromEdk2RelativePath(
|
---|
77 | packagename)
|
---|
78 |
|
---|
79 | if abs_pkg_path is None:
|
---|
80 | tc.SetSkipped()
|
---|
81 | tc.LogStdError("No package {0}".format(packagename))
|
---|
82 | return -1
|
---|
83 |
|
---|
84 | # check for node
|
---|
85 | return_buffer = StringIO()
|
---|
86 | ret = RunCmd("node", "--version", outstream=return_buffer)
|
---|
87 | if (ret != 0):
|
---|
88 | tc.SetSkipped()
|
---|
89 | tc.LogStdError("NodeJs not installed. Test can't run")
|
---|
90 | logging.warning("NodeJs not installed. Test can't run")
|
---|
91 | return -1
|
---|
92 | node_version = return_buffer.getvalue().strip() # format vXX.XX.XX
|
---|
93 | tc.LogStdOut(f"Node version: {node_version}")
|
---|
94 | version_aggregator.GetVersionAggregator().ReportVersion(
|
---|
95 | "NodeJs", node_version, version_aggregator.VersionTypes.INFO)
|
---|
96 |
|
---|
97 | # Check for cspell
|
---|
98 | return_buffer = StringIO()
|
---|
99 | ret = RunCmd("cspell", "--version", outstream=return_buffer)
|
---|
100 | if (ret != 0):
|
---|
101 | tc.SetSkipped()
|
---|
102 | tc.LogStdError("cspell not installed. Test can't run")
|
---|
103 | logging.warning("cspell not installed. Test can't run")
|
---|
104 | return -1
|
---|
105 | cspell_version = return_buffer.getvalue().strip() # format XX.XX.XX
|
---|
106 | tc.LogStdOut(f"CSpell version: {cspell_version}")
|
---|
107 | version_aggregator.GetVersionAggregator().ReportVersion(
|
---|
108 | "CSpell", cspell_version, version_aggregator.VersionTypes.INFO)
|
---|
109 |
|
---|
110 | # copy the default as a list
|
---|
111 | package_relative_paths_to_spell_check = list(SpellCheck.STANDARD_PLUGIN_DEFINED_PATHS)
|
---|
112 |
|
---|
113 | #
|
---|
114 | # Allow the ci.yaml to remove any of the above standard paths
|
---|
115 | #
|
---|
116 | if("IgnoreStandardPaths" in pkgconfig):
|
---|
117 | for a in pkgconfig["IgnoreStandardPaths"]:
|
---|
118 | if(a in package_relative_paths_to_spell_check):
|
---|
119 | tc.LogStdOut(
|
---|
120 | f"ignoring standard path due to ci.yaml ignore: {a}")
|
---|
121 | package_relative_paths_to_spell_check.remove(a)
|
---|
122 | else:
|
---|
123 | tc.LogStdOut(f"Invalid IgnoreStandardPaths value: {a}")
|
---|
124 |
|
---|
125 | #
|
---|
126 | # check for any additional include paths defined by package config
|
---|
127 | #
|
---|
128 | if("AdditionalIncludePaths" in pkgconfig):
|
---|
129 | package_relative_paths_to_spell_check.extend(
|
---|
130 | pkgconfig["AdditionalIncludePaths"])
|
---|
131 |
|
---|
132 | #
|
---|
133 | # Make the path string for cspell to check
|
---|
134 | #
|
---|
135 | relpath = os.path.relpath(abs_pkg_path)
|
---|
136 | cpsell_paths = " ".join(
|
---|
137 | # Double quote each path to defer expansion to cspell parameters
|
---|
138 | [f'"{relpath}/**/{x}"' for x in package_relative_paths_to_spell_check])
|
---|
139 |
|
---|
140 | # Make the config file
|
---|
141 | config_file_path = os.path.join(
|
---|
142 | Edk2pathObj.WorkspacePath, "Build", packagename, "cspell_actual_config.json")
|
---|
143 | mydir = os.path.dirname(os.path.abspath(__file__))
|
---|
144 | # load as yaml so it can have comments
|
---|
145 | base = os.path.join(mydir, "cspell.base.yaml")
|
---|
146 | with open(base, "r") as i:
|
---|
147 | config = yaml.safe_load(i)
|
---|
148 |
|
---|
149 | if("ExtendWords" in pkgconfig):
|
---|
150 | config["words"].extend(pkgconfig["ExtendWords"])
|
---|
151 | with open(config_file_path, "w") as o:
|
---|
152 | json.dump(config, o) # output as json so compat with cspell
|
---|
153 |
|
---|
154 | All_Ignores = []
|
---|
155 | # Parse the config for other ignores
|
---|
156 | if "IgnoreFiles" in pkgconfig:
|
---|
157 | All_Ignores.extend(pkgconfig["IgnoreFiles"])
|
---|
158 |
|
---|
159 | # spell check all the files
|
---|
160 | ignore = parse_gitignore_lines(All_Ignores, os.path.join(
|
---|
161 | abs_pkg_path, "nofile.txt"), abs_pkg_path)
|
---|
162 |
|
---|
163 | # result is a list of strings like this
|
---|
164 | # C:\src\sp-edk2\edk2\FmpDevicePkg\FmpDevicePkg.dec:53:9 - Unknown word (Capule)
|
---|
165 | EasyFix = []
|
---|
166 | results = self._check_spelling(cpsell_paths, config_file_path)
|
---|
167 | for r in results:
|
---|
168 | path, _, word = r.partition(" - Unknown word ")
|
---|
169 | if len(word) == 0:
|
---|
170 | # didn't find pattern
|
---|
171 | continue
|
---|
172 |
|
---|
173 | pathinfo = path.rsplit(":", 2) # remove the line no info
|
---|
174 | if(ignore(pathinfo[0])): # check against ignore list
|
---|
175 | tc.LogStdOut(f"ignoring error due to ci.yaml ignore: {r}")
|
---|
176 | continue
|
---|
177 |
|
---|
178 | # real error
|
---|
179 | EasyFix.append(word.strip().strip("()"))
|
---|
180 | Errors.append(r)
|
---|
181 |
|
---|
182 | # Log all errors tc StdError
|
---|
183 | for l in Errors:
|
---|
184 | tc.LogStdError(l.strip())
|
---|
185 |
|
---|
186 | # Helper - Log the syntax needed to add these words to dictionary
|
---|
187 | if len(EasyFix) > 0:
|
---|
188 | EasyFix = sorted(set(a.lower() for a in EasyFix))
|
---|
189 | logging.error(f'SpellCheck found {len(EasyFix)} failing words. See CI log for details.')
|
---|
190 | tc.LogStdOut("\n Easy fix:")
|
---|
191 | OneString = "If these are not errors add this to your ci.yaml file.\n"
|
---|
192 | OneString += '"SpellCheck": {\n "ExtendWords": ['
|
---|
193 | for a in EasyFix:
|
---|
194 | tc.LogStdOut(f'\n"{a}",')
|
---|
195 | OneString += f'\n "{a}",'
|
---|
196 | logging.critical(OneString.rstrip(",") + '\n ]\n}')
|
---|
197 |
|
---|
198 | # add result to test case
|
---|
199 | overall_status = len(Errors)
|
---|
200 | if overall_status != 0:
|
---|
201 | if "AuditOnly" in pkgconfig and pkgconfig["AuditOnly"]:
|
---|
202 | # set as skipped if AuditOnly
|
---|
203 | tc.SetSkipped()
|
---|
204 | return -1
|
---|
205 | else:
|
---|
206 | tc.SetFailed("SpellCheck {0} Failed. Errors {1}".format(
|
---|
207 | packagename, overall_status), "CHECK_FAILED")
|
---|
208 | else:
|
---|
209 | tc.SetSuccess()
|
---|
210 | return overall_status
|
---|
211 |
|
---|
212 | def _check_spelling(self, abs_file_to_check: str, abs_config_file_to_use: str) -> []:
|
---|
213 | output = StringIO()
|
---|
214 | ret = RunCmd(
|
---|
215 | "cspell", f"--config {abs_config_file_to_use} {abs_file_to_check}", outstream=output)
|
---|
216 | if ret == 0:
|
---|
217 | return []
|
---|
218 | else:
|
---|
219 | return output.getvalue().strip().splitlines()
|
---|