# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import json
import os
import re
import signal
import subprocess
import sys
sys.path.append(os.path.join(os.path.dirname(__file__),
"eslint"))
from eslint
import prettier_utils, setup_helper
from mozbuild.nodeutil
import check_node_executables_valid, find_node_executable
from mozlint
import result
STYLELINT_ERROR_MESSAGE =
"""
An error occurred running stylelint. Please check the following error messages:
{}
""".strip()
STYLELINT_NOT_FOUND_MESSAGE =
"""
Could
not find stylelint! We looked at the --binary option, at the STYLELINT
environment variable,
and then at your local node_modules path. Please install
eslint, stylelint
and needed plugins
with:
mach eslint --setup
and try again.
""".strip()
FILE_EXT_REGEX = re.compile(r
"\.[a-z0-9_]{2,10}$", re.IGNORECASE)
def setup(root, **lintargs):
setup_helper.set_project_root(root)
if not check_node_executables_valid():
return 1
return setup_helper.eslint_maybe_setup()
def lint(paths, config, binary=
None, fix=
None, rules=[], setup=
None, **lintargs):
"""Run stylelint."""
log = lintargs[
"log"]
setup_helper.set_project_root(lintargs[
"root"])
module_path = setup_helper.get_project_root()
modified_paths = []
exts =
"*.{" +
",".join(config[
"extensions"]) +
"}"
for path
in paths:
filepath, fileext = os.path.splitext(path)
if fileext:
modified_paths += [path]
else:
joined_path = os.path.join(path,
"**", exts)
if prettier_utils.is_windows():
joined_path = joined_path.replace(
"\\",
"/")
modified_paths.append(joined_path)
# Valid binaries are:
# - Any provided by the binary argument.
# - Any pointed at by the STYLELINT environmental variable.
# - Those provided by |mach lint --setup|.
if not binary:
binary, _ = find_node_executable()
if not binary:
print(STYLELINT_NOT_FOUND_MESSAGE)
return 1
extra_args = lintargs.get(
"extra_args")
or []
result = {
"results": [],
"fixed":
0}
# If something passes formatonly, avoid running stylelint and only run prettier.
# This allows ./mach format and similar mechanisms to work correctly.
if not lintargs.get(
"formatonly",
False):
exclude_args = []
for path
in config.get(
"exclude", []):
exclude_args.extend([
"--ignore-pattern",
os.path.relpath(path, lintargs[
"root"]),
])
# Default to $topsrcdir/.stylelintrc.js, but allow override in stylelint.yml
stylelint_rc = config.get(
"stylelint-rc",
".stylelintrc.js")
# First run Stylelint
cmd_args = (
[
binary,
os.path.join(
module_path,
"node_modules",
"stylelint",
"bin",
"stylelint.mjs"
),
"--formatter",
"json",
"--allow-empty-input",
"--config",
os.path.join(lintargs[
"root"], stylelint_rc),
]
+ extra_args
+ exclude_args
+ modified_paths
)
if fix:
cmd_args.append(
"--fix")
log.debug(
"Stylelint command: {}".format(
" ".join(cmd_args)))
# Set up environment for stylelint subprocess
env = os.environ.copy()
if lintargs.get(
"skip_rollouts",
False):
env[
"STYLELINT_SKIP_ROLLOUTS"] =
"1"
result = run(cmd_args, config, fix, env)
if result ==
1:
return result
# Then run Prettier
cmd_args = (
[
binary,
os.path.join(
module_path,
"node_modules",
"prettier",
"bin",
"prettier.cjs"
),
"--list-different",
"--no-error-on-unmatched-pattern",
"--ignore-path=.prettierignore",
"--ignore-path=.prettierignore-non-css",
]
# Prettier does not support exclude arguments.
# + exclude_args
+ paths
)
log.debug(
"Prettier command: {}".format(
" ".join(cmd_args)))
if fix:
cmd_args.append(
"--write")
prettier_result = prettier_utils.run_prettier(cmd_args, config, fix)
if prettier_result ==
1:
return prettier_result
result[
"results"].extend(prettier_result[
"results"])
result[
"fixed"] = result[
"fixed"] + prettier_result[
"fixed"]
return result
def run(cmd_args, config, fix, env):
shell =
False
if prettier_utils.is_windows():
# The stylelint binary needs to be run from a shell with msys
shell =
True
encoding =
"utf-8"
orig = signal.signal(signal.SIGINT, signal.SIG_IGN)
proc = subprocess.Popen(
cmd_args, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env
)
signal.signal(signal.SIGINT, orig)
try:
_, errors = proc.communicate()
except KeyboardInterrupt:
proc.kill()
return {
"results": [],
"fixed":
0}
if errors:
errors = errors.decode(encoding,
"replace")
# 0 is success, 2 is there was at least 1 rule violation. Anything else
# is more serious.
if proc.returncode
not in {
0,
2}:
if proc.returncode ==
78:
print(
"Stylelint reported an issue with its configuration file.")
print(errors)
return 1
if not errors:
return {
"results": [],
"fixed":
0}
try:
jsonresult = json.loads(errors)
except ValueError:
print(STYLELINT_ERROR_MESSAGE.format(errors))
return 1
results = []
fixed =
0
for obj
in jsonresult:
errors = obj[
"warnings"] + obj[
"parseErrors"]
# This will return a number of fixed files, as that's the only thing
# stylelint gives us. Note that it also seems to sometimes list files
# like this where it finds nothing and fixes nothing. It's not clear
# why... but this is why we also check if we were even trying to fix
# anything.
if fix
and not errors
and not obj.get(
"ignored"):
fixed +=
1
for err
in errors:
msg = err.get(
"text")
if err.get(
"rule"):
# stylelint includes the rule id in the error message.
# All mozlint formatters that include the error message also already
# separately include the rule id, so that leads to duplication. Fix:
msg = msg.replace(
"(" + err.get(
"rule") +
")",
"").strip()
err.update({
"message": msg,
"level": err.get(
"severity")
or "error",
"lineno": err.get(
"line")
or 0,
"path": obj[
"source"],
"rule": err.get(
"rule")
or "parseError",
})
results.append(result.from_config(config, **err))
return {
"results": results,
"fixed": fixed}