mirror of https://github.com/acidanthera/audk.git
218 lines
9.0 KiB
Python
218 lines
9.0 KiB
Python
# @file SpellCheck.py
|
|
#
|
|
# An edk2-pytool based plugin wrapper for cspell
|
|
#
|
|
# Copyright (c) Microsoft Corporation.
|
|
# SPDX-License-Identifier: BSD-2-Clause-Patent
|
|
##
|
|
import logging
|
|
import json
|
|
import yaml
|
|
from io import StringIO
|
|
import os
|
|
from edk2toolext.environment.plugintypes.ci_build_plugin import ICiBuildPlugin
|
|
from edk2toollib.utility_functions import RunCmd
|
|
from edk2toolext.environment.var_dict import VarDict
|
|
from edk2toollib.gitignore_parser import parse_gitignore_lines
|
|
from edk2toolext.environment import version_aggregator
|
|
|
|
|
|
class SpellCheck(ICiBuildPlugin):
|
|
"""
|
|
A CiBuildPlugin that uses the cspell node module to scan the files
|
|
from the package being tested for spelling errors. The plugin contains
|
|
the base cspell.json file then thru the configuration options other settings
|
|
can be changed or extended.
|
|
|
|
Configuration options:
|
|
"SpellCheck": {
|
|
"AuditOnly": False, # Don't fail the build if there are errors. Just log them
|
|
"IgnoreFiles": [], # use gitignore syntax to ignore errors in matching files
|
|
"ExtendWords": [], # words to extend to the dictionary for this package
|
|
"IgnoreStandardPaths": [], # Standard Plugin defined paths that should be ignore
|
|
"AdditionalIncludePaths": [] # Additional paths to spell check (wildcards supported)
|
|
}
|
|
"""
|
|
|
|
#
|
|
# A package can remove any of these using IgnoreStandardPaths
|
|
#
|
|
STANDARD_PLUGIN_DEFINED_PATHS = ("*.c", "*.h",
|
|
"*.nasm", "*.asm", "*.masm", "*.s",
|
|
"*.asl",
|
|
"*.dsc", "*.dec", "*.fdf", "*.inf",
|
|
"*.md", "*.txt"
|
|
)
|
|
|
|
def GetTestName(self, packagename: str, environment: VarDict) -> tuple:
|
|
""" Provide the testcase name and classname for use in reporting
|
|
|
|
Args:
|
|
packagename: string containing name of package to build
|
|
environment: The VarDict for the test to run in
|
|
Returns:
|
|
a tuple containing the testcase name and the classname
|
|
(testcasename, classname)
|
|
testclassname: a descriptive string for the testcase can include whitespace
|
|
classname: should be patterned <packagename>.<plugin>.<optionally any unique condition>
|
|
"""
|
|
return ("Spell check files in " + packagename, packagename + ".SpellCheck")
|
|
|
|
##
|
|
# External function of plugin. This function is used to perform the task of the CiBuild Plugin
|
|
#
|
|
# - package is the edk2 path to package. This means workspace/packagepath relative.
|
|
# - edk2path object configured with workspace and packages path
|
|
# - PkgConfig Object (dict) for the pkg
|
|
# - EnvConfig Object
|
|
# - Plugin Manager Instance
|
|
# - Plugin Helper Obj Instance
|
|
# - Junit Logger
|
|
# - output_stream the StringIO output stream from this plugin via logging
|
|
|
|
def RunBuildPlugin(self, packagename, Edk2pathObj, pkgconfig, environment, PLM, PLMHelper, tc, output_stream=None):
|
|
Errors = []
|
|
|
|
abs_pkg_path = Edk2pathObj.GetAbsolutePathOnThisSytemFromEdk2RelativePath(
|
|
packagename)
|
|
|
|
if abs_pkg_path is None:
|
|
tc.SetSkipped()
|
|
tc.LogStdError("No package {0}".format(packagename))
|
|
return -1
|
|
|
|
# check for node
|
|
return_buffer = StringIO()
|
|
ret = RunCmd("node", "--version", outstream=return_buffer)
|
|
if (ret != 0):
|
|
tc.SetSkipped()
|
|
tc.LogStdError("NodeJs not installed. Test can't run")
|
|
logging.warning("NodeJs not installed. Test can't run")
|
|
return -1
|
|
node_version = return_buffer.getvalue().strip() # format vXX.XX.XX
|
|
tc.LogStdOut(f"Node version: {node_version}")
|
|
version_aggregator.GetVersionAggregator().ReportVersion(
|
|
"NodeJs", node_version, version_aggregator.VersionTypes.INFO)
|
|
|
|
# Check for cspell
|
|
return_buffer = StringIO()
|
|
ret = RunCmd("cspell", "--version", outstream=return_buffer)
|
|
if (ret != 0):
|
|
tc.SetSkipped()
|
|
tc.LogStdError("cspell not installed. Test can't run")
|
|
logging.warning("cspell not installed. Test can't run")
|
|
return -1
|
|
cspell_version = return_buffer.getvalue().strip() # format XX.XX.XX
|
|
tc.LogStdOut(f"CSpell version: {cspell_version}")
|
|
version_aggregator.GetVersionAggregator().ReportVersion(
|
|
"CSpell", cspell_version, version_aggregator.VersionTypes.INFO)
|
|
|
|
# copy the default as a list
|
|
package_relative_paths_to_spell_check = list(SpellCheck.STANDARD_PLUGIN_DEFINED_PATHS)
|
|
|
|
#
|
|
# Allow the ci.yaml to remove any of the above standard paths
|
|
#
|
|
if("IgnoreStandardPaths" in pkgconfig):
|
|
for a in pkgconfig["IgnoreStandardPaths"]:
|
|
if(a in package_relative_paths_to_spell_check):
|
|
tc.LogStdOut(
|
|
f"ignoring standard path due to ci.yaml ignore: {a}")
|
|
package_relative_paths_to_spell_check.remove(a)
|
|
else:
|
|
tc.LogStdOut(f"Invalid IgnoreStandardPaths value: {a}")
|
|
|
|
#
|
|
# check for any additional include paths defined by package config
|
|
#
|
|
if("AdditionalIncludePaths" in pkgconfig):
|
|
package_relative_paths_to_spell_check.extend(
|
|
pkgconfig["AdditionalIncludePaths"])
|
|
|
|
#
|
|
# Make the path string for cspell to check
|
|
#
|
|
relpath = os.path.relpath(abs_pkg_path)
|
|
cpsell_paths = " ".join(
|
|
[f"{relpath}/**/{x}" for x in package_relative_paths_to_spell_check])
|
|
|
|
# Make the config file
|
|
config_file_path = os.path.join(
|
|
Edk2pathObj.WorkspacePath, "Build", packagename, "cspell_actual_config.json")
|
|
mydir = os.path.dirname(os.path.abspath(__file__))
|
|
# load as yaml so it can have comments
|
|
base = os.path.join(mydir, "cspell.base.yaml")
|
|
with open(base, "r") as i:
|
|
config = yaml.safe_load(i)
|
|
|
|
if("ExtendWords" in pkgconfig):
|
|
config["words"].extend(pkgconfig["ExtendWords"])
|
|
with open(config_file_path, "w") as o:
|
|
json.dump(config, o) # output as json so compat with cspell
|
|
|
|
All_Ignores = []
|
|
# Parse the config for other ignores
|
|
if "IgnoreFiles" in pkgconfig:
|
|
All_Ignores.extend(pkgconfig["IgnoreFiles"])
|
|
|
|
# spell check all the files
|
|
ignore = parse_gitignore_lines(All_Ignores, os.path.join(
|
|
abs_pkg_path, "nofile.txt"), abs_pkg_path)
|
|
|
|
# result is a list of strings like this
|
|
# C:\src\sp-edk2\edk2\FmpDevicePkg\FmpDevicePkg.dec:53:9 - Unknown word (Capule)
|
|
EasyFix = []
|
|
results = self._check_spelling(cpsell_paths, config_file_path)
|
|
for r in results:
|
|
path, _, word = r.partition(" - Unknown word ")
|
|
if len(word) == 0:
|
|
# didn't find pattern
|
|
continue
|
|
|
|
pathinfo = path.rsplit(":", 2) # remove the line no info
|
|
if(ignore(pathinfo[0])): # check against ignore list
|
|
tc.LogStdOut(f"ignoring error due to ci.yaml ignore: {r}")
|
|
continue
|
|
|
|
# real error
|
|
EasyFix.append(word.strip().strip("()"))
|
|
Errors.append(r)
|
|
|
|
# Log all errors tc StdError
|
|
for l in Errors:
|
|
tc.LogStdError(l.strip())
|
|
|
|
# Helper - Log the syntax needed to add these words to dictionary
|
|
if len(EasyFix) > 0:
|
|
EasyFix = sorted(set(a.lower() for a in EasyFix))
|
|
tc.LogStdOut("\n Easy fix:")
|
|
OneString = "If these are not errors add this to your ci.yaml file.\n"
|
|
OneString += '"SpellCheck": {\n "ExtendWords": ['
|
|
for a in EasyFix:
|
|
tc.LogStdOut(f'\n"{a}",')
|
|
OneString += f'\n "{a}",'
|
|
logging.info(OneString.rstrip(",") + '\n ]\n}')
|
|
|
|
# add result to test case
|
|
overall_status = len(Errors)
|
|
if overall_status != 0:
|
|
if "AuditOnly" in pkgconfig and pkgconfig["AuditOnly"]:
|
|
# set as skipped if AuditOnly
|
|
tc.SetSkipped()
|
|
return -1
|
|
else:
|
|
tc.SetFailed("SpellCheck {0} Failed. Errors {1}".format(
|
|
packagename, overall_status), "CHECK_FAILED")
|
|
else:
|
|
tc.SetSuccess()
|
|
return overall_status
|
|
|
|
def _check_spelling(self, abs_file_to_check: str, abs_config_file_to_use: str) -> []:
|
|
output = StringIO()
|
|
ret = RunCmd(
|
|
"cspell", f"--config {abs_config_file_to_use} {abs_file_to_check}", outstream=output)
|
|
if ret == 0:
|
|
return []
|
|
else:
|
|
return output.getvalue().strip().splitlines()
|