# 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
from filecmp
import dircmp
from mozbuild.nodeutil
import (
package_setup,
remove_directory,
)
from packaging.version
import Version
VERSION_RE = re.compile(r
"^\d+\.\d+\.\d+$")
CARET_VERSION_RANGE_RE = re.compile(r
"^\^((\d+)\.\d+\.\d+)$")
_state = {
"project_root":
None}
def eslint_maybe_setup(package_root=
None, package_name=
None):
"""Setup ESLint only if it is needed."""
if package_root
is None:
package_root = get_project_root()
if package_name
is None:
package_name =
"eslint"
has_issues, needs_clobber = eslint_module_needs_setup(package_root, package_name)
if has_issues:
eslint_setup(package_root, package_name, needs_clobber)
def eslint_setup(package_root, package_name, should_clobber=
False):
"""Ensure eslint is optimally configured.
This command will inspect your eslint configuration
and
guide you through an interactive wizard helping you configure
eslint
for optimal use on Mozilla projects.
"""
# Always remove the eslint-plugin-mozilla sub-directory as that can
# sometimes conflict with the top level node_modules, see bug 1809036.
remove_directory(
os.path.join(get_eslint_module_path(),
"eslint-plugin-mozilla",
"node_modules")
)
orig_project_root = get_project_root()
try:
set_project_root(package_root)
package_setup(package_root, package_name, should_clobber=should_clobber)
finally:
set_project_root(orig_project_root)
def expected_installed_modules(package_root, package_name):
# Read the expected version of ESLint and external modules
expected_modules_path = os.path.join(package_root,
"package.json")
with open(expected_modules_path, encoding=
"utf-8")
as f:
sections = json.load(f)
expected_modules = sections.get(
"dependencies", {})
expected_modules.update(sections.get(
"devDependencies", {}))
if package_name ==
"eslint":
# Also read the in-tree ESLint plugin mozilla information, to ensure the
# dependencies are up to date.
mozilla_json_path = os.path.join(
get_eslint_module_path(),
"eslint-plugin-mozilla",
"package.json"
)
with open(mozilla_json_path, encoding=
"utf-8")
as f:
dependencies = json.load(f).get(
"dependencies", {})
expected_modules.update(dependencies)
# Also read the in-tree ESLint plugin spidermonkey information, to ensure the
# dependencies are up to date.
mozilla_json_path = os.path.join(
get_eslint_module_path(),
"eslint-plugin-spidermonkey-js",
"package.json"
)
with open(mozilla_json_path, encoding=
"utf-8")
as f:
expected_modules.update(json.load(f).get(
"dependencies", {}))
return expected_modules
def check_eslint_files(node_modules_path, name):
def check_file_diffs(dcmp):
# Diff files only looks at files that are different. Not for files
# that are only present on one side. This should be generally OK as
# new files will need to be added in the index.js for the package.
if dcmp.diff_files
and dcmp.diff_files != [
"package.json"]:
return True
result =
False
# Again, we only look at common sub directories for the same reason
# as above.
for sub_dcmp
in dcmp.subdirs.values():
result = result
or check_file_diffs(sub_dcmp)
return result
dcmp = dircmp(
os.path.join(node_modules_path, name),
os.path.join(get_eslint_module_path(), name),
)
return check_file_diffs(dcmp)
def eslint_module_needs_setup(package_root, package_name):
has_issues =
False
needs_clobber =
False
node_modules_path = os.path.join(package_root,
"node_modules")
for name, expected_data
in expected_installed_modules(
package_root, package_name
).items():
# expected_installed_modules package_root, package_namereturns a string for the version number of
# dependencies for installation of eslint generally, and an object
# for our in-tree plugins (which contains the entire module info).
if "version" in expected_data:
version_range = expected_data[
"version"]
else:
version_range = expected_data
path = os.path.join(node_modules_path, name,
"package.json")
if not os.path.exists(path):
print(f
"{name} v{version_range} needs to be installed locally.")
has_issues =
True
continue
data = json.load(open(path, encoding=
"utf-8"))
if version_range.startswith(
"file:")
or version_range.startswith(
"github:"):
# We don't need to check local file installations for versions, as
# these are symlinked, so we'll always pick up the latest.
# For github versions, these are hard to sync, so we'll assume some other
# module gets updated at the same time for now.
continue
if name ==
"eslint" and Version(
"4.0.0") > Version(data[
"version"]):
print(
"ESLint is an old version, clobbering node_modules directory")
needs_clobber =
True
has_issues =
True
continue
if not version_in_range(data[
"version"], version_range):
print(
"{} v{} should be v{}.".format(name, data[
"version"], version_range))
has_issues =
True
continue
return has_issues, needs_clobber
def version_in_range(version, version_range):
"""
Check
if a module version
is inside a version range. Only supports explicit versions
and
caret ranges
for the moment, since that
's all we've used so far.
"""
if version == version_range:
return True
version_match = VERSION_RE.match(version)
if not version_match:
raise RuntimeError(f
"mach eslint doesn't understand module version {version}")
version = Version(version)
# Caret ranges as specified by npm allow changes that do not modify the left-most non-zero
# digit in the [major, minor, patch] tuple. The code below assumes the major digit is
# non-zero.
range_match = CARET_VERSION_RANGE_RE.match(version_range)
if range_match:
range_version = range_match.group(
1)
range_major = int(range_match.group(
2))
range_min = Version(range_version)
range_max = Version(f
"{range_major + 1}.0.0")
return range_min <= version < range_max
return False
def set_project_root(root=
None):
"""Sets the project root to the supplied path, or works out what the root
is based on looking
for 'mach'.
Keyword arguments:
root - (optional) The path to set the root to.
"""
if root:
_state[
"project_root"] = root
return
file_found =
False
folder = os.getcwd()
while folder:
if os.path.exists(os.path.join(folder,
"mach")):
file_found =
True
break
else:
folder = os.path.dirname(folder)
if file_found:
_state[
"project_root"] = os.path.abspath(folder)
def get_project_root():
"""Returns the absolute path to the root of the project, see set_project_root()
for how this
is determined.
"""
if not _state[
"project_root"]:
set_project_root()
return _state[
"project_root"]
def get_eslint_module_path():
return os.path.join(get_project_root(),
"tools",
"lint",
"eslint")