Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / tools / lint / stylelint / __init__.py
blob7c1b9271b963f656a7d07450161dfae3c50d3fdb
1 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
2 # vim: set filetype=python:
3 # This Source Code Form is subject to the terms of the Mozilla Public
4 # License, v. 2.0. If a copy of the MPL was not distributed with this
5 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 import json
8 import os
9 import re
10 import signal
11 import subprocess
12 import sys
14 sys.path.append(os.path.join(os.path.dirname(__file__), "eslint"))
15 from eslint import setup_helper
16 from mozbuild.nodeutil import find_node_executable
17 from mozlint import result
19 STYLELINT_ERROR_MESSAGE = """
20 An error occurred running stylelint. Please check the following error messages:
23 """.strip()
25 STYLELINT_NOT_FOUND_MESSAGE = """
26 Could not find stylelint! We looked at the --binary option, at the STYLELINT
27 environment variable, and then at your local node_modules path. Please install
28 eslint, stylelint and needed plugins with:
30 mach eslint --setup
32 and try again.
33 """.strip()
35 FILE_EXT_REGEX = re.compile(r"\.[a-z0-9_]{2,10}$", re.IGNORECASE)
38 def setup(root, **lintargs):
39 setup_helper.set_project_root(root)
41 if not setup_helper.check_node_executables_valid():
42 return 1
44 return setup_helper.eslint_maybe_setup()
47 def lint(paths, config, binary=None, fix=None, rules=[], setup=None, **lintargs):
48 """Run stylelint."""
49 log = lintargs["log"]
50 setup_helper.set_project_root(lintargs["root"])
51 module_path = setup_helper.get_project_root()
53 modified_paths = []
54 exts = "*.{" + ",".join(config["extensions"]) + "}"
56 for path in paths:
57 filepath, fileext = os.path.splitext(path)
58 if fileext:
59 modified_paths += [path]
60 else:
61 joined_path = os.path.join(path, "**", exts)
62 if is_windows():
63 joined_path = joined_path.replace("\\", "/")
64 modified_paths.append(joined_path)
66 # Valid binaries are:
67 # - Any provided by the binary argument.
68 # - Any pointed at by the STYLELINT environmental variable.
69 # - Those provided by |mach lint --setup|.
71 if not binary:
72 binary, _ = find_node_executable()
74 if not binary:
75 print(STYLELINT_NOT_FOUND_MESSAGE)
76 return 1
78 extra_args = lintargs.get("extra_args") or []
79 exclude_args = []
80 for path in config.get("exclude", []):
81 exclude_args.extend(
82 ["--ignore-pattern", os.path.relpath(path, lintargs["root"])]
85 # Default to $topsrcdir/.stylelintrc.js, but allow override in stylelint.yml
86 stylelint_rc = config.get("stylelint-rc", ".stylelintrc.js")
88 # First run Stylelint
89 cmd_args = (
91 binary,
92 os.path.join(
93 module_path, "node_modules", "stylelint", "bin", "stylelint.mjs"
95 "--formatter",
96 "json",
97 "--allow-empty-input",
98 "--config",
99 os.path.join(lintargs["root"], stylelint_rc),
101 + extra_args
102 + exclude_args
103 + modified_paths
106 if fix:
107 cmd_args.append("--fix")
109 log.debug("Stylelint command: {}".format(" ".join(cmd_args)))
111 result = run(cmd_args, config, fix)
112 if result == 1:
113 return result
115 return result
118 def run(cmd_args, config, fix):
119 shell = False
120 if is_windows():
121 # The stylelint binary needs to be run from a shell with msys
122 shell = True
123 encoding = "utf-8"
125 orig = signal.signal(signal.SIGINT, signal.SIG_IGN)
126 proc = subprocess.Popen(
127 cmd_args, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE
129 signal.signal(signal.SIGINT, orig)
131 try:
132 _, errors = proc.communicate()
133 except KeyboardInterrupt:
134 proc.kill()
135 return {"results": [], "fixed": 0}
137 if errors:
138 errors = errors.decode(encoding, "replace")
140 # 0 is success, 2 is there was at least 1 rule violation. Anything else
141 # is more serious.
142 if proc.returncode != 0 and proc.returncode != 2:
143 if proc.returncode == 78:
144 print("Stylelint reported an issue with its configuration file.")
145 print(errors)
146 return 1
148 if not errors:
149 return {"results": [], "fixed": 0}
151 try:
152 jsonresult = json.loads(errors)
153 except ValueError:
154 print(STYLELINT_ERROR_MESSAGE.format(errors))
155 return 1
157 results = []
158 fixed = 0
159 for obj in jsonresult:
160 errors = obj["warnings"] + obj["parseErrors"]
161 # This will return a number of fixed files, as that's the only thing
162 # stylelint gives us. Note that it also seems to sometimes list files
163 # like this where it finds nothing and fixes nothing. It's not clear
164 # why... but this is why we also check if we were even trying to fix
165 # anything.
166 if fix and not errors and not obj.get("ignored"):
167 fixed += 1
169 for err in errors:
170 msg = err.get("text")
171 if err.get("rule"):
172 # stylelint includes the rule id in the error message.
173 # All mozlint formatters that include the error message also already
174 # separately include the rule id, so that leads to duplication. Fix:
175 msg = msg.replace("(" + err.get("rule") + ")", "").strip()
176 err.update(
178 "message": msg,
179 "level": err.get("severity") or "error",
180 "lineno": err.get("line") or 0,
181 "path": obj["source"],
182 "rule": err.get("rule") or "parseError",
185 results.append(result.from_config(config, **err))
187 return {"results": results, "fixed": fixed}
190 def is_windows():
191 return (
192 os.environ.get("MSYSTEM") in ("MINGW32", "MINGW64")
193 or "MOZILLABUILD" in os.environ