# 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")